package wordkey import ( "bytes" "crypto/sha256" "fmt" "math" "strings" "testing" ) // The built-in lists and their SHA-256, as lists/README.md records them. A // change of a list changes the hash an app pins, so it is never silent. var listHashes = map[string]string{ "en": "6d557f0693958fb5e650b68b5bee585eb82cf4da32965505c789e924743bc522", "es": "ff77b487765c000da97cca58fe94a2cdb947303e7a07460614d7d95d800034fe", } func TestBuiltInLists(t *testing.T) { if got := strings.Join(Languages(), " "); got != "en es" { t.Fatalf("Languages() = %q", got) } for lang, want := range listHashes { if got := fmt.Sprintf("%x", sha256.Sum256([]byte(lists[lang]))); got != want { t.Errorf("list %s: SHA-256 %s, want %s", lang, got, want) } words, err := List(lang) if err != nil { t.Fatal(err) } if len(words) != 7776 { t.Errorf("list %s: %d words, want 7776", lang, len(words)) } } if _, err := List("xx"); err == nil || !strings.Contains(err.Error(), `no word list for "xx"; the lists are en, es`) { t.Errorf("List(xx): %v", err) } // The list of the EFF keeps its order, so that the word at position i // is the one of the dice of i in base 6: 11111 is the first, and 66666 // the last. en, _ := List("en") if en[0] != "abacus" || en[1] != "abdomen" || en[7775] != "zoom" { t.Errorf("list en: %q, %q, %q", en[0], en[1], en[7775]) } } func TestCheckList(t *testing.T) { // pal followed by three letters: palaaa, palaab, … base := make([]string, MinListSize) for i := range base { base[i] = "pal" + string([]rune{'a' + rune(i/676), 'a' + rune(i/26%26), 'a' + rune(i%26)}) } if err := CheckList("es", base); err != nil { t.Fatal(err) } if err := CheckList("xx", base); err == nil || err.Error() != `no alphabet for the language "xx"` { t.Errorf("CheckList(xx): %v", err) } with := func(i int, w string) []string { l := append([]string(nil), base...) l[i] = w return l } for _, c := range []struct { list []string want string }{ {base[:MinListSize-1], "2047 words, fewer than 2048"}, {with(5, "dos palabras"), `line 6, "dos palabras", is not one word`}, {with(5, " "), "is not one word"}, {with(5, "mi"), `"mi", is not one word of 3 or more letters`}, {with(5, "casa​"), "invisible character U+200B"}, // Only the letters of the alphabet of the language, as the list // writes them: no capitals, no Cyrillic U+0441 that looks like a Latin // c, no digits, no carriage return of a file with CRLF lines. {with(5, "Palaaf"), `line 6, "Palaaf", holds U+0050, which is not in the alphabet of "es"`}, {with(5, string(rune(0x0441))+"asa"), "holds U+0441, which is not in the alphabet"}, {with(5, "pal1"), "holds U+0031"}, {with(5, "palaaf\r"), `line 6, "palaaf\r", holds U+000D`}, {with(5, base[4]), `line 6, "palaae", is the same word as "palaae"`}, {with(5, "palaáe"), `line 6, "palaáe", is the same word as "palaae"`}, {append(with(0, "papá"), "papa"), `"papa", is the same word as "papá"`}, } { if err := CheckList("es", c.list); err == nil || !strings.Contains(err.Error(), c.want) { t.Errorf("CheckList: %v, want %q", err, c.want) } } // The hyphen of the compound words of the list of the EFF is a letter of // en and not of es; an accent is a letter of es and not of en. if err := CheckList("en", with(5, "t-shirt")); err != nil { t.Errorf("CheckList(en) with t-shirt: %v", err) } for _, c := range []struct{ lang, word, want string }{ {"es", "t-shirt", `line 6, "t-shirt", holds U+002D, which is not in the alphabet of "es"`}, {"en", "palaáf", `line 6, "palaáf", holds U+00E1, which is not in the alphabet of "en"`}, } { if err := CheckList(c.lang, with(5, c.word)); err == nil || err.Error() != c.want { t.Errorf("CheckList(%s) with %q: %v, want %q", c.lang, c.word, err, c.want) } } } func TestGenerate(t *testing.T) { list, err := List("es") if err != nil { t.Fatal(err) } words, err := Generate(list, DefaultCount, nil) if err != nil { t.Fatal(err) } if len(words) != DefaultCount { t.Fatalf("%d words", len(words)) } if err := Check(Normalize(strings.Join(words, " "))); err != nil { t.Errorf("generated words %q: %v", words, err) } seen := map[string]bool{} for _, w := range words { if seen[w] { t.Errorf("%q drawn twice", w) } seen[w] = true } // The same random bytes draw the same words: Generate reads nothing else. var seed []byte for i := range 8 { h := sha256.Sum256([]byte{byte(i)}) seed = append(seed, h[:]...) } a, err := Generate(list, 6, bytes.NewReader(seed)) if err != nil { t.Fatal(err) } b, err := Generate(list, 6, bytes.NewReader(seed)) if err != nil || len(a) != 6 || strings.Join(a, " ") != strings.Join(b, " ") { t.Errorf("%q and %q: %v", a, b, err) } // An index drawn twice is drawn again: each pair of bytes is an index // below 8192, and 0 1 is index 1. again := []byte{0, 1, 0, 1, 0, 2, 0, 3, 0, 4, 0, 5, 0, 6} if w, err := Generate(list, 6, bytes.NewReader(again)); err != nil || strings.Join(w, " ") != strings.Join(list[1:7], " ") { t.Errorf("Generate with index 1 twice: %q, %v", w, err) } // Bytes that only ever give two indices, 1793 and 2081, run out. if _, err := Generate(list, 6, bytes.NewReader(bytes.Repeat([]byte{7, 1, 200, 33}, 64))); err == nil || !strings.Contains(err.Error(), "EOF") { t.Errorf("Generate with two indices: %v", err) } for _, c := range []struct { n int want string }{ {5, "at least 6 words, not 5"}, {len(list), "7776 words of a list of 7776"}, } { if _, err := Generate(list, c.n, nil); err == nil || !strings.Contains(err.Error(), c.want) { t.Errorf("Generate(%d): %v, want %q", c.n, err, c.want) } } if _, err := Generate(list, 6, bytes.NewReader(nil)); err == nil || !strings.Contains(err.Error(), "EOF") { t.Errorf("Generate without random bytes: %v", err) } } func TestBits(t *testing.T) { if b := Bits(7776, 1); b != math.Log2(7776) { t.Errorf("Bits(7776, 1) = %v", b) } // 7 words of 7776 are a little under 90.5 bits, and 6 of 2048, the // fewest that Generate draws, a little under 66. for _, c := range []struct { size, count int low, high float64 }{ {7776, 0, 0, 0}, {7776, 7, 90.469, 90.470}, {7776, 8, 103.393, 103.394}, {2048, 6, 65.989, 65.990}, } { if b := Bits(c.size, c.count); b < c.low || b > c.high { t.Errorf("Bits(%d, %d) = %v", c.size, c.count, b) } } } // Every word is about as likely: over 7776·40 draws of one word, each index // falls in its bucket of 64 between 0.8 and 1.2 times the mean. func TestGenerateUniform(t *testing.T) { if testing.Short() { t.Skip("slow") } list, _ := List("es") index := make(map[string]int, len(list)) for i, w := range list { index[w] = i } const buckets = 64 var count [buckets]int draws := 0 for draws < len(list)*40 { words, err := Generate(list, 6, nil) if err != nil { t.Fatal(err) } for _, w := range words { count[index[w]*buckets/len(list)]++ draws++ } } mean := float64(draws) / buckets for i, c := range count { if float64(c) < 0.8*mean || float64(c) > 1.2*mean { t.Errorf("bucket %d: %d draws, mean %.0f", i, c, mean) } } }