package configgen import ( "math" "strings" "testing" ) // R-597 — ENGLISH WORDS FOR ENGLISH HOUSEHOLDS, AND NEVER A WEAKER CODE. // // The 2026-09-20 English drill received a setup code of three Hungarian words with accents inside an // otherwise English e-mail (`képző-szkítia-ásatás`). It can be pasted; it cannot be read aloud, and // it cannot be retyped by someone who does not have the accents on their keyboard. // // The English list is smaller than the Hungarian one, so "translate the code" is not free: fewer // bits per word. These tests exist so the fix cannot quietly buy readability with security. // S3 — for every use, the English code carries at least as many bits as the Hungarian one. // // NOT A CONSTANT-FOR-MEASUREMENT DECOY: both sides are computed from the embedded lists' actual // lengths and the shipped count table. Shrink english.txt, or drop a word count in wordCounts, and // this fails. (Red-proofed by setting UseSetupCode's "en" to 3 — see the session REPORT.) func TestEnglishIsNeverWeakerThanHungarian(t *testing.T) { if len(wordList) < 2 || len(englishList) < 2 { t.Fatalf("a wordlist did not load: hu=%d en=%d", len(wordList), len(englishList)) } for _, use := range []Use{UseSetupCode, UseOwnerPassphrase} { hu := EntropyBitsFor("hu", use) en := EntropyBitsFor("en", use) if hu <= 0 || en <= 0 { t.Fatalf("%s: entropy came out zero (hu=%.2f en=%.2f) — the table or a list is missing", use, hu, en) } if en < hu { t.Errorf("%s: the ENGLISH code is WEAKER than the Hungarian one — en %d words = %.2f bits, "+ "hu %d words = %.2f bits. An English household must not be given a code that is easier "+ "to guess in exchange for being readable.", use, WordCountFor("en", use), en, WordCountFor("hu", use), hu) } t.Logf("%-18s hu %d words = %6.2f bits | en %d words = %6.2f bits (%.2f bits/word hu, %.2f en)", use, WordCountFor("hu", use), hu, WordCountFor("en", use), en, math.Log2(float64(len(wordList))), math.Log2(float64(len(englishList)))) } } // The Hungarian side is UNCHANGED. Not "still fine" — identical: same list, same counts, so every // Hungarian household's code is exactly what it was before v0.119.0. func TestHungarianCodesUnchanged(t *testing.T) { if got := WordCountFor("hu", UseSetupCode); got != 3 { t.Errorf("the Hungarian setup code is now %d words, was 3", got) } if got := WordCountFor("hu", UseOwnerPassphrase); got != 5 { t.Errorf("the Hungarian owner passphrase is now %d words, was 5", got) } // The Hungarian list itself: the file has 29634 lines with 25 duplicates. Pinned so a list swap // cannot move the Hungarian entropy floor without saying so. if len(wordList) != 29609 { t.Errorf("the Hungarian list is %d words, was 29609 — the entropy floor moved", len(wordList)) } // And a Hungarian code must still be drawn from the Hungarian list. code, err := RandomPassphraseFor("hu", UseSetupCode) if err != nil { t.Fatal(err) } hu := make(map[string]struct{}, len(wordList)) for _, w := range wordList { hu[w] = struct{}{} } for _, w := range strings.Split(code, passphraseSep) { if _, ok := hu[w]; !ok { t.Errorf("a Hungarian setup code contains %q, which is not in the Hungarian list", w) } } } // An English code is drawn from the English list, has the right number of words, and segments back // into exactly that many — the joinSafe guarantee. func TestEnglishCodeIsEnglishAndSegments(t *testing.T) { en := make(map[string]struct{}, len(englishList)) for _, w := range englishList { en[w] = struct{}{} } for _, use := range []Use{UseSetupCode, UseOwnerPassphrase} { want := WordCountFor("en", use) for i := 0; i < 200; i++ { code, err := RandomPassphraseFor("en", use) if err != nil { t.Fatalf("%s: %v", use, err) } parts := strings.Split(code, passphraseSep) if len(parts) != want { t.Fatalf("%s: code %q segments into %d words, want %d — a word carrying the separator "+ "slipped past joinSafe", use, code, len(parts), want) } for _, w := range parts { if _, ok := en[w]; !ok { t.Fatalf("%s: code contains %q, which is not in the English list", use, w) } } } } } // What the discarded "phone rule" was actually reaching for, kept as an assertion instead of a // filter (see joinSafe's note). A word that carries a digit, an accent, a capital or a separator is // the thing that genuinely breaks transcription and retyping. func TestEnglishListIsTranscribable(t *testing.T) { if len(englishList) != 7772 { t.Errorf("the English list is %d words, expected 7772 (EFF large, minus the four hyphenated "+ "entries) — the entropy floor moved", len(englishList)) } for _, w := range englishList { if len(w) < 3 || len(w) > 9 { t.Errorf("%q is %d characters — outside the 3-9 range a person can hold in their head", w, len(w)) } for _, r := range w { if r < 'a' || r > 'z' { t.Errorf("%q contains %q — an English code must be lower-case ASCII letters only, so it "+ "can be typed on any keyboard, which is the whole reason this list exists", w, r) break } } } } // The ENTIRE POINT of an English code is that it survives a round trip through the box's comparison, // which lower-cases and re-joins. A household who types their code with spaces, or in capitals, must // be let in. func TestEnglishCodeSurvivesNormalisation(t *testing.T) { code, err := RandomPassphraseFor("en", UseSetupCode) if err != nil { t.Fatal(err) } for _, typed := range []string{ code, strings.ToUpper(code), strings.ReplaceAll(code, passphraseSep, " "), " " + strings.ReplaceAll(code, passphraseSep, " ") + " ", } { if got := NormalizePassphrase(typed); got != code { t.Errorf("typing %q normalises to %q, want %q", typed, got, code) } } } // An unsupported or empty language must land on Hungarian — the list AND the count together. A // caller that got the list right and the count wrong would produce a code weaker than either. func TestUnknownLanguageFallsBackToHungarianWholesale(t *testing.T) { for _, lang := range []string{"", "de", "EN-GB", "xx"} { if got, want := WordCountFor(lang, UseSetupCode), WordCountFor("hu", UseSetupCode); got != want { t.Errorf("language %q: %d words, want the Hungarian %d", lang, got, want) } code, err := RandomPassphraseFor(lang, UseSetupCode) if err != nil { t.Fatalf("language %q: %v", lang, err) } if n := len(strings.Split(code, passphraseSep)); n != WordCountFor("hu", UseSetupCode) { t.Errorf("language %q produced a %d-word code", lang, n) } } } // An unknown USE must be an ERROR, never a silently short code. This is the direction that matters: // a typo'd Use returning a one-word passphrase would be a catastrophic silent weakening. func TestUnknownUseIsRefused(t *testing.T) { if code, err := RandomPassphraseFor("en", Use("retrieval_key")); err == nil { t.Errorf("an unknown use produced a passphrase %q instead of an error", code) } }