hub v0.119.0 — English households get English words for their codes (R-597); R-596/R-598 closed
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
This commit is contained in:
2026-09-21 07:56:56 +02:00
parent a499327236
commit e02bc03819
18 changed files with 8859 additions and 50 deletions
+18 -8
View File
@@ -16,10 +16,15 @@ import (
"golang.org/x/crypto/bcrypt"
)
// codeWords is the claim/reset code length: 3 Hungarian words (~44 bits with the ~29K list) —
// dictatable over the phone, and the controller's 5-attempt/15-min lockout makes online guessing
// infeasible.
const codeWords = 3
// The claim/reset code length is no longer a constant here: it is per language, and it lives in ONE
// place — configgen.wordCounts, read through configgen.WordCountFor (R-597, v0.119.0).
//
// It used to be `const codeWords = 3` with the note "3 Hungarian words (~44 bits with the ~29K
// list) — dictatable over the phone". Every word of that is still true for a Hungarian household,
// and their code is unchanged. It was never true for an English one: they got the same three
// Hungarian words, accents and all, inside an English e-mail. The English code is four words from
// the EFF list (~51.7 bits — MORE than the Hungarian 44.6, never less), because the English list is
// smaller. The controller's 5-attempt/15-minute lockout is unchanged and language-blind.
// maxResetPerDay is the hub-side cap on controller-forwarded reset requests (per customer).
const maxResetPerDay = 3
@@ -67,9 +72,14 @@ func (e *Engine) logf(f string, a ...any) {
}
}
// newCode generates a fresh code and its bcrypt hash.
func newCode() (code, hash string, err error) {
code, err = configgen.RandomPassphrase(codeWords)
// newCode generates a fresh code and its bcrypt hash, in the household's language.
//
// The BOX is untouched by this: it stores and compares a bcrypt hash of whatever was minted, with no
// notion of which list the words came from (controller internal/web/claim.go). So an English code is
// accepted by exactly the same path a Hungarian one is, including the TTL, the single-use
// generation and the lockout.
func newCode(lang string) (code, hash string, err error) {
code, err = configgen.RandomPassphraseFor(lang, configgen.UseSetupCode)
if err != nil {
return "", "", fmt.Errorf("claim: generating code: %w", err)
}
@@ -84,7 +94,7 @@ func newCode() (code, hash string, err error) {
// keeps the rotated hash (the gate stays armed) and is LOUD: the operator sees emailed_at unset
// on the customer page and can resend. Returns the new generation.
func (e *Engine) rotateAndSend(cc *store.CustomerConfig, kind EmailKind) (int, error) {
code, hash, err := newCode()
code, hash, err := newCode(e.Store.CustomerLanguage(cc.CustomerID))
if err != nil {
return 0, err
}