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

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

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.