You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
DateKeys/wordkey/generate.go

151 lines
4.8 KiB

package wordkey
import (
"crypto/rand"
_ "embed"
"fmt"
"io"
"math"
"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/en.txt
var listEN string
//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{
"en": listEN,
"es": listES,
}
// alphabets are the letters that a word of a list of each language may hold,
// as the list writes it: lower case, in NFC. A word with a letter of another
// script that looks like one of these, such as the Cyrillic U+0430 for the
// Latin a, would be written down and typed again with the letter of the
// keyboard, and the capsule would not open. The alphabet of a list comes from
// here, never from the list. The English one has the ASCII hyphen of the four
// compound words of the list of the EFF, such as t-shirt, kept so that every
// word keeps its number of dice.
var alphabets = map[string]string{
"en": "abcdefghijklmnopqrstuvwxyz-",
"es": "abcdefghijklmnopqrstuvwxyzáéíóúüñ",
}
// 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(lang, 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 of the language lang for
// Generate: a language without an alphabet in this package; fewer than
// MinListSize words; a word that is not one word of MinLetters characters or
// more once normalized, that holds a character Check refuses or one that is
// not a letter of the alphabet of lang; or a word 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(lang string, words []string) error {
alphabet, ok := alphabets[lang]
if !ok {
return fmt.Errorf("no alphabet for the language %q", lang)
}
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)
}
for _, r := range w {
if !strings.ContainsRune(alphabet, r) {
return fmt.Errorf("line %d, %q, holds U+%04X, which is not in the alphabet of %q", i+1, w, r, lang)
}
}
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
}
// Bits is the strength of count words that Generate draws from a list of
// size words: log2 of the number of draws in order, size·(size−1)·…, which
// whoever knows the list must search, before the rounds of PBKDF2.
func Bits(size, count int) float64 {
b := 0.0
for i := range count {
b += math.Log2(float64(size - i))
}
return b
}

Powered by TurnKey Linux.