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