e02bc03819
gates / gates (push) Successful in 24s
The setup code and the owner passphrase now follow the household's language,
one word longer in English so the entropy never drops (setup 3 hu / 4 en,
passphrase 5 hu / 6 en). List and count are chosen together so a caller cannot
pair an English list with a Hungarian count. Hungarian is byte-unchanged.
Three claims in the row were wrong and are recorded as such:
- the RECOVERY CODE is minted by felhom-agent from the EFF list and has
always been English; the hub does not own it and no row was added.
- no claim mail states a word count; the only count wording was the bind
page's passphrase hint, whose English half is now count-free.
- the proposed phone-safe filter removes 68% of the list (5270 of 7772
words) and was measured, then declined, with the reason in source.
Also: guide_quote_gate binds the English volunteer guide's three quoted
messages to the controller's English bundle — nothing did, so the guide would
have gone on quoting Hungarian after the fix. Seven decoys, all convicting,
including the name-for-fact one.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
169 lines
6.8 KiB
Go
169 lines
6.8 KiB
Go
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)
|
|
}
|
|
}
|