escrow: a recovery code can no longer contain a hyphenated word (v0.93.0)
The EFF large list has exactly 4 entries containing the join separator (drop-down, felt-tip, t-shirt, yo-yo). Drawing one made a code read as 11 words instead of 10 - ambiguous to transcribe in precisely the situation R exists for. Filter them at init; the draw space goes 7776 -> 7772 and the 10-word code goes 129.248 -> 129.241 bits, still well over the 128 floor. Generation-only: already-issued codes stay valid, R is verified as a whole passphrase and never re-split. Also fixes the ~1/5 flake this same defect caused: the test counted words by splitting the joined string. It now counts what the generator drew and asserts segmentation separately, plus a deterministic red-proof fixture.
This commit is contained in:
@@ -11,8 +11,16 @@ import (
|
||||
)
|
||||
|
||||
func TestWordlistLoaded(t *testing.T) {
|
||||
if WordlistSize() != 7776 {
|
||||
t.Fatalf("EFF large wordlist should be 7776 words, got %d", WordlistSize())
|
||||
// The EFF large list is 7776 entries; joinSafe removes the 4 that contain RecoveryCodeSep
|
||||
// (drop-down, felt-tip, t-shirt, yo-yo), leaving 7772 as the effective draw space.
|
||||
if got := WordlistSize(); got != 7772 {
|
||||
t.Fatalf("effective wordlist should be 7772 words (7776 EFF - 4 hyphenated), got %d", got)
|
||||
}
|
||||
if got := WordlistFilteredOut(); got != 4 {
|
||||
t.Fatalf("joinSafe should have removed exactly 4 hyphenated entries, removed %d", got)
|
||||
}
|
||||
if got := WordlistSize() + WordlistFilteredOut(); got != 7776 {
|
||||
t.Fatalf("filtered + removed should reconstitute the 7776-word EFF list, got %d", got)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,19 +33,27 @@ func TestGenerateRecoveryCode_EntropyAndFormat(t *testing.T) {
|
||||
inList[w] = true
|
||||
}
|
||||
for i := 0; i < 50; i++ {
|
||||
r, err := GenerateRecoveryCode()
|
||||
// Count words by GENERATION count, not by re-splitting the joined string: the two agree
|
||||
// only because joinSafe holds, and conflating them is what made this test flake ~1/5.
|
||||
words, err := generateWords(wordlist)
|
||||
if err != nil {
|
||||
t.Fatalf("GenerateRecoveryCode: %v", err)
|
||||
t.Fatalf("generateWords: %v", err)
|
||||
}
|
||||
words := strings.Split(r, "-")
|
||||
if len(words) != RecoveryCodeWords {
|
||||
t.Fatalf("recovery code must be %d words, got %d (%q)", RecoveryCodeWords, len(words), r)
|
||||
t.Fatalf("generator must draw %d words, drew %d", RecoveryCodeWords, len(words))
|
||||
}
|
||||
for _, w := range words {
|
||||
if !inList[w] {
|
||||
t.Errorf("recovery-code word %q is not from the EFF wordlist", w)
|
||||
}
|
||||
}
|
||||
// Separately assert the property joinSafe buys: the joined code segments back to the same
|
||||
// count. Never print r — it is a live-shaped secret.
|
||||
r := strings.Join(words, RecoveryCodeSep)
|
||||
if got := len(strings.Split(r, RecoveryCodeSep)); got != RecoveryCodeWords {
|
||||
t.Fatalf("joined code must segment into %d words, got %d (a drawn word contained %q)",
|
||||
RecoveryCodeWords, got, RecoveryCodeSep)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user