wordkey.Generate draws words uniformly with crypto/rand from a built-in list, and encrypt -new-words FILE writes them to a new file (-dic, default es; -word-count, default 7). wordkey.List and CheckList refuse a list of fewer than 2048 words or with two words that are one once normalized (spec 38.1). The Spanish list, 7776 words, is a draft not yet reviewed, licensed CC BY-SA 4.0 as an adaptation of FrequencyWords; its source, method and SHA-256 are in wordkey/lists/README.md. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>v0.15
parent
fe50885491
commit
c49c67ce83
@ -0,0 +1,110 @@
|
|||||||
|
package wordkey
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/rand"
|
||||||
|
_ "embed"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"math/big"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
"unicode/utf8"
|
||||||
|
)
|
||||||
|
|
||||||
|
// DefaultCount is the number of words Generate draws when the caller does
|
||||||
|
// not ask for more: 7 words of a list of 7776 are about 90 bits.
|
||||||
|
const DefaultCount = 7
|
||||||
|
|
||||||
|
// MinListSize is the fewest words of a list that Generate accepts (spec
|
||||||
|
// §38.1: at least 6 words of a list of 2048 or more).
|
||||||
|
const MinListSize = 2048
|
||||||
|
|
||||||
|
//go:embed lists/es.txt
|
||||||
|
var listES string
|
||||||
|
|
||||||
|
// lists are the word lists built into the module, by language. Each is a
|
||||||
|
// plain UTF-8 file, one word per line; lists/README.md says where each comes
|
||||||
|
// from and its license.
|
||||||
|
var lists = map[string]string{
|
||||||
|
"es": listES,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Languages returns the languages of the built-in word lists, sorted.
|
||||||
|
func Languages() []string {
|
||||||
|
var l []string
|
||||||
|
for k := range lists {
|
||||||
|
l = append(l, k)
|
||||||
|
}
|
||||||
|
sort.Strings(l)
|
||||||
|
return l
|
||||||
|
}
|
||||||
|
|
||||||
|
// List returns the built-in word list of lang, after checking it with
|
||||||
|
// CheckList.
|
||||||
|
func List(lang string) ([]string, error) {
|
||||||
|
text, ok := lists[lang]
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("wordkey: no word list for %q; the lists are %s", lang, strings.Join(Languages(), ", "))
|
||||||
|
}
|
||||||
|
words := strings.Split(strings.TrimSuffix(text, "\n"), "\n")
|
||||||
|
if err := CheckList(words); err != nil {
|
||||||
|
return nil, fmt.Errorf("wordkey: the list %q: %w", lang, err)
|
||||||
|
}
|
||||||
|
return words, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CheckList reports why words cannot be a list for Generate: fewer than
|
||||||
|
// MinListSize words, or a word that is not one word of MinLetters
|
||||||
|
// characters or more once normalized, that holds a character Check refuses,
|
||||||
|
// or that is the same as another once normalized. Two words such as «papa»
|
||||||
|
// and «papá» would be one word with less entropy than the list promises.
|
||||||
|
func CheckList(words []string) error {
|
||||||
|
if len(words) < MinListSize {
|
||||||
|
return fmt.Errorf("%d words, fewer than %d", len(words), MinListSize)
|
||||||
|
}
|
||||||
|
seen := make(map[string]string, len(words))
|
||||||
|
for i, w := range words {
|
||||||
|
n := Normalize(w)
|
||||||
|
if len(n) != 1 || n[0] != strings.TrimSpace(n[0]) || utf8.RuneCountInString(n[0]) < MinLetters {
|
||||||
|
return fmt.Errorf("line %d, %q, is not one word of %d or more letters", i+1, w, MinLetters)
|
||||||
|
}
|
||||||
|
if err := checkRunes(n[0]); err != nil {
|
||||||
|
return fmt.Errorf("line %d: %w", i+1, err)
|
||||||
|
}
|
||||||
|
if prev, ok := seen[n[0]]; ok {
|
||||||
|
return fmt.Errorf("line %d, %q, is the same word as %q once normalized", i+1, w, prev)
|
||||||
|
}
|
||||||
|
seen[n[0]] = w
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate draws n different words of list, uniformly, with random, which
|
||||||
|
// is crypto/rand.Reader when nil. Each word adds log2(len(list)) bits, a
|
||||||
|
// little less for each word already drawn. n must be MinWords or more.
|
||||||
|
func Generate(list []string, n int, random io.Reader) ([]string, error) {
|
||||||
|
if n < MinWords {
|
||||||
|
return nil, fmt.Errorf("wordkey: a key of words needs at least %d words, not %d", MinWords, n)
|
||||||
|
}
|
||||||
|
if n > len(list)/2 {
|
||||||
|
return nil, fmt.Errorf("wordkey: %d words of a list of %d", n, len(list))
|
||||||
|
}
|
||||||
|
if random == nil {
|
||||||
|
random = rand.Reader
|
||||||
|
}
|
||||||
|
picked := make(map[int]bool, n)
|
||||||
|
words := make([]string, 0, n)
|
||||||
|
size := big.NewInt(int64(len(list)))
|
||||||
|
for len(words) < n {
|
||||||
|
i, err := rand.Int(random, size)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("wordkey: %w", err)
|
||||||
|
}
|
||||||
|
if picked[int(i.Int64())] {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
picked[int(i.Int64())] = true
|
||||||
|
words = append(words, list[i.Int64()])
|
||||||
|
}
|
||||||
|
return words, nil
|
||||||
|
}
|
||||||
@ -0,0 +1,38 @@
|
|||||||
|
# Word lists of `wordkey.Generate`
|
||||||
|
|
||||||
|
The lists from which `datekeys encrypt -new-words` draws a key of words at
|
||||||
|
random (spec §38.1: at least 6 words of a public list of 2048 or more). They
|
||||||
|
are not normative: a reader does not need them, because the key is derived
|
||||||
|
from the normalized text of the words, whatever list they came from.
|
||||||
|
|
||||||
|
| List | Words | SHA-256 | Status |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `es.txt` | 7776 | `ff77b487765c000da97cca58fe94a2cdb947303e7a07460614d7d95d800034fe` | Draft, not yet reviewed by a native speaker |
|
||||||
|
|
||||||
|
A list changes only with its hash in this file and in `generate_test.go`:
|
||||||
|
an application that downloads a list pins its SHA-256 and refuses any other.
|
||||||
|
|
||||||
|
## `es.txt`
|
||||||
|
|
||||||
|
- **Source:** the frequencies of
|
||||||
|
[FrequencyWords](https://github.com/hermitdave/FrequencyWords) by Hermit
|
||||||
|
Dave, `content/2018/es/es_50k.txt`, counted on the OpenSubtitles 2018
|
||||||
|
corpus, licensed under CC BY-SA 4.0. The Spanish Hunspell dictionary of
|
||||||
|
LibreOffice (RLA-ES, `es_ES.dic` and `es_ES.aff`) is used only as a filter
|
||||||
|
and is not redistributed.
|
||||||
|
- **License:** this list is an adaptation of FrequencyWords and is licensed
|
||||||
|
under [CC BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/),
|
||||||
|
unlike the code of this module (Apache 2.0).
|
||||||
|
- **Method:** the most frequent words that are, in order:
|
||||||
|
1. lowercase letters only, of 3 to 9 letters;
|
||||||
|
2. a base form of the dictionary: an entry with affix flags, or the
|
||||||
|
feminine its flag `G` makes. Entries without flags, which are
|
||||||
|
conjugations, plurals and pieces of names, are left out;
|
||||||
|
3. not in a short list of offensive or unpleasant words;
|
||||||
|
4. not the other half of a pair that differs only in a final `-o` or `-a`
|
||||||
|
(`chico` and `chica`): the more frequent stays;
|
||||||
|
5. not the same as a word already taken once normalized as in §38.1
|
||||||
|
(`papa` and `papá`): the more frequent stays.
|
||||||
|
|
||||||
|
The first 7776 that pass, sorted. `wordkey.CheckList` checks the last rule
|
||||||
|
and the characters of every word.
|
||||||
File diff suppressed because it is too large
Load Diff
Loading…
Reference in new issue