|
|
|
|
|
package wordkey
|
|
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
|
"bytes"
|
|
|
|
|
|
"crypto/sha256"
|
|
|
|
|
|
"fmt"
|
The alphabet of a word list and the strength of the words drawn
wordkey.CheckList takes the language of the list and refuses a word with a
character that is not a letter of its alphabet, which only the code gives:
for es, a to z, the five vowels with an acute accent, u with diaeresis and
n with tilde, in lower case and NFC. A Cyrillic letter that looks like a
Latin one, a capital, a digit or the carriage return of a file with CRLF
lines would be written down and typed again with another character, and
the capsule would not open. The Spanish list passes as it is; its SHA-256
does not change.
wordkey.Bits is the strength of the words that Generate draws, log2 of the
number of draws in order, and encrypt -new-words prints it: 90 bits for 7
words of 7776.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
12 hours ago
|
|
|
|
"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{
|
The English list: the large wordlist of the EFF, and -dic en by default
wordkey/lists/en.txt is the large wordlist for passphrases of the EFF,
7776 words, CC BY 4.0 under its copyright policy, without the dice number
of each line and in its order, so that the position of a word still gives
its dice, 11111 for the first. encrypt -new-words takes it by default, as
the author decided; -dic es takes the Spanish one. The alphabet of en is a
to z and the ASCII hyphen of its four compound words, such as t-shirt,
kept so that no word loses its dice. lists/README.md records its source,
the SHA-256 of the download, the change and the license.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
11 hours ago
|
|
|
|
"en": "6d557f0693958fb5e650b68b5bee585eb82cf4da32965505c789e924743bc522",
|
|
|
|
|
|
"es": "ff77b487765c000da97cca58fe94a2cdb947303e7a07460614d7d95d800034fe",
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func TestBuiltInLists(t *testing.T) {
|
The English list: the large wordlist of the EFF, and -dic en by default
wordkey/lists/en.txt is the large wordlist for passphrases of the EFF,
7776 words, CC BY 4.0 under its copyright policy, without the dice number
of each line and in its order, so that the position of a word still gives
its dice, 11111 for the first. encrypt -new-words takes it by default, as
the author decided; -dic es takes the Spanish one. The alphabet of en is a
to z and the ASCII hyphen of its four compound words, such as t-shirt,
kept so that no word loses its dice. lists/README.md records its source,
the SHA-256 of the download, the change and the license.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
11 hours ago
|
|
|
|
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))
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
The English list: the large wordlist of the EFF, and -dic en by default
wordkey/lists/en.txt is the large wordlist for passphrases of the EFF,
7776 words, CC BY 4.0 under its copyright policy, without the dice number
of each line and in its order, so that the position of a word still gives
its dice, 11111 for the first. encrypt -new-words takes it by default, as
the author decided; -dic es takes the Spanish one. The alphabet of en is a
to z and the ASCII hyphen of its four compound words, such as t-shirt,
kept so that no word loses its dice. lists/README.md records its source,
the SHA-256 of the download, the change and the license.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
11 hours ago
|
|
|
|
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 English list: the large wordlist of the EFF, and -dic en by default
wordkey/lists/en.txt is the large wordlist for passphrases of the EFF,
7776 words, CC BY 4.0 under its copyright policy, without the dice number
of each line and in its order, so that the position of a word still gives
its dice, 11111 for the first. encrypt -new-words takes it by default, as
the author decided; -dic es takes the Spanish one. The alphabet of en is a
to z and the ASCII hyphen of its four compound words, such as t-shirt,
kept so that no word loses its dice. lists/README.md records its source,
the SHA-256 of the download, the change and the license.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
11 hours ago
|
|
|
|
// 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) {
|
The alphabet of a word list and the strength of the words drawn
wordkey.CheckList takes the language of the list and refuses a word with a
character that is not a letter of its alphabet, which only the code gives:
for es, a to z, the five vowels with an acute accent, u with diaeresis and
n with tilde, in lower case and NFC. A Cyrillic letter that looks like a
Latin one, a capital, a digit or the carriage return of a file with CRLF
lines would be written down and typed again with another character, and
the capsule would not open. The Spanish list passes as it is; its SHA-256
does not change.
wordkey.Bits is the strength of the words that Generate draws, log2 of the
number of draws in order, and encrypt -new-words prints it: 90 bits for 7
words of 7776.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
12 hours ago
|
|
|
|
// pal followed by three letters: palaaa, palaab, …
|
|
|
|
|
|
base := make([]string, MinListSize)
|
|
|
|
|
|
for i := range base {
|
The alphabet of a word list and the strength of the words drawn
wordkey.CheckList takes the language of the list and refuses a word with a
character that is not a letter of its alphabet, which only the code gives:
for es, a to z, the five vowels with an acute accent, u with diaeresis and
n with tilde, in lower case and NFC. A Cyrillic letter that looks like a
Latin one, a capital, a digit or the carriage return of a file with CRLF
lines would be written down and typed again with another character, and
the capsule would not open. The Spanish list passes as it is; its SHA-256
does not change.
wordkey.Bits is the strength of the words that Generate draws, log2 of the
number of draws in order, and encrypt -new-words prints it: 90 bits for 7
words of 7776.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
12 hours ago
|
|
|
|
base[i] = "pal" + string([]rune{'a' + rune(i/676), 'a' + rune(i/26%26), 'a' + rune(i%26)})
|
|
|
|
|
|
}
|
The alphabet of a word list and the strength of the words drawn
wordkey.CheckList takes the language of the list and refuses a word with a
character that is not a letter of its alphabet, which only the code gives:
for es, a to z, the five vowels with an acute accent, u with diaeresis and
n with tilde, in lower case and NFC. A Cyrillic letter that looks like a
Latin one, a capital, a digit or the carriage return of a file with CRLF
lines would be written down and typed again with another character, and
the capsule would not open. The Spanish list passes as it is; its SHA-256
does not change.
wordkey.Bits is the strength of the words that Generate draws, log2 of the
number of draws in order, and encrypt -new-words prints it: 90 bits for 7
words of 7776.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
12 hours ago
|
|
|
|
if err := CheckList("es", base); err != nil {
|
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
|
}
|
The alphabet of a word list and the strength of the words drawn
wordkey.CheckList takes the language of the list and refuses a word with a
character that is not a letter of its alphabet, which only the code gives:
for es, a to z, the five vowels with an acute accent, u with diaeresis and
n with tilde, in lower case and NFC. A Cyrillic letter that looks like a
Latin one, a capital, a digit or the carriage return of a file with CRLF
lines would be written down and typed again with another character, and
the capsule would not open. The Spanish list passes as it is; its SHA-256
does not change.
wordkey.Bits is the strength of the words that Generate draws, log2 of the
number of draws in order, and encrypt -new-words prints it: 90 bits for 7
words of 7776.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
12 hours ago
|
|
|
|
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"},
|
The alphabet of a word list and the strength of the words drawn
wordkey.CheckList takes the language of the list and refuses a word with a
character that is not a letter of its alphabet, which only the code gives:
for es, a to z, the five vowels with an acute accent, u with diaeresis and
n with tilde, in lower case and NFC. A Cyrillic letter that looks like a
Latin one, a capital, a digit or the carriage return of a file with CRLF
lines would be written down and typed again with another character, and
the capsule would not open. The Spanish list passes as it is; its SHA-256
does not change.
wordkey.Bits is the strength of the words that Generate draws, log2 of the
number of draws in order, and encrypt -new-words prints it: 90 bits for 7
words of 7776.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
12 hours ago
|
|
|
|
// 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á"`},
|
|
|
|
|
|
} {
|
The alphabet of a word list and the strength of the words drawn
wordkey.CheckList takes the language of the list and refuses a word with a
character that is not a letter of its alphabet, which only the code gives:
for es, a to z, the five vowels with an acute accent, u with diaeresis and
n with tilde, in lower case and NFC. A Cyrillic letter that looks like a
Latin one, a capital, a digit or the carriage return of a file with CRLF
lines would be written down and typed again with another character, and
the capsule would not open. The Spanish list passes as it is; its SHA-256
does not change.
wordkey.Bits is the strength of the words that Generate draws, log2 of the
number of draws in order, and encrypt -new-words prints it: 90 bits for 7
words of 7776.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
12 hours ago
|
|
|
|
if err := CheckList("es", c.list); err == nil || !strings.Contains(err.Error(), c.want) {
|
|
|
|
|
|
t.Errorf("CheckList: %v, want %q", err, c.want)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
The English list: the large wordlist of the EFF, and -dic en by default
wordkey/lists/en.txt is the large wordlist for passphrases of the EFF,
7776 words, CC BY 4.0 under its copyright policy, without the dice number
of each line and in its order, so that the position of a word still gives
its dice, 11111 for the first. encrypt -new-words takes it by default, as
the author decided; -dic es takes the Spanish one. The alphabet of en is a
to z and the ASCII hyphen of its four compound words, such as t-shirt,
kept so that no word loses its dice. lists/README.md records its source,
the SHA-256 of the download, the change and the license.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
11 hours ago
|
|
|
|
// 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)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
The alphabet of a word list and the strength of the words drawn
wordkey.CheckList takes the language of the list and refuses a word with a
character that is not a letter of its alphabet, which only the code gives:
for es, a to z, the five vowels with an acute accent, u with diaeresis and
n with tilde, in lower case and NFC. A Cyrillic letter that looks like a
Latin one, a capital, a digit or the carriage return of a file with CRLF
lines would be written down and typed again with another character, and
the capsule would not open. The Spanish list passes as it is; its SHA-256
does not change.
wordkey.Bits is the strength of the words that Generate draws, log2 of the
number of draws in order, and encrypt -new-words prints it: 90 bits for 7
words of 7776.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
12 hours ago
|
|
|
|
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)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|