80 lines
3.9 KiB
Markdown
80 lines
3.9 KiB
Markdown
# REPORT — escrow recovery-code wordlist fix (TASK-E Part 1), 2026-07-21
|
||
|
||
**Version: v0.93.0. Baseline `935904f` (v0.92.1), clean tree. NOT built or deployed** — no operator
|
||
action needed today; this rides the next agent publish train.
|
||
|
||
## What was wrong
|
||
|
||
The EFF large wordlist contains exactly four entries that themselves contain the hyphen the words are
|
||
joined with: `drop-down` (L2009), `felt-tip` (L2528), `t-shirt` (L6640), `yo-yo` (L7748). Drawing one
|
||
produced a recovery code that reads as **11 words instead of 10** — ambiguous to transcribe in the one
|
||
situation R exists for, a customer reading a code back during a disaster.
|
||
|
||
**The "known flake" was this defect, not a flaky test.** Per code the hit rate is
|
||
`1 − (7772/7776)^10 ≈ 0.51%`; over the test's 50 draws that is ≈ **23%**, which is exactly the "fails
|
||
~1/5" that had been documented in the `felhom-testing` skill and re-run past for weeks.
|
||
`felhom.eu/REPORT.md` §6 had it at 3/8 in one session.
|
||
|
||
## The fix
|
||
|
||
`joinSafe` filters those four out of the effective wordlist at init. Generation-only.
|
||
|
||
| | before | after |
|
||
|---|---|---|
|
||
| draw space | 7776 | **7772** |
|
||
| 10-word code entropy | 129.248 bits | **129.241 bits** |
|
||
| margin over the 128-bit floor | 1.248 | **1.241** |
|
||
|
||
The cost is **0.007 bits**. Both numbers are asserted in `TestEntropyFloorSurvivesFiltering`, so a
|
||
future wordlist swap cannot quietly move the floor.
|
||
|
||
**Every recovery code already issued remains valid**, and this was verified rather than assumed: R is
|
||
consumed as a whole passphrase by the PBS scrypt KDF (`Wrap`/`Unwrap`), and `grep` confirms nothing in
|
||
the consume path ever splits it. The joiner, the word count and the KDF path are untouched.
|
||
|
||
## Tests
|
||
|
||
`TestGenerateRecoveryCode_EntropyAndFormat` now counts words by **generation count**, not by splitting
|
||
the joined string — conflating those two is what made it flake. It asserts the segmentation property
|
||
separately, because that is the property `joinSafe` actually buys.
|
||
|
||
New in `wordlist_test.go`: the four filtered words are pinned by name (a wordlist swap that changes the
|
||
set fails loudly); the entropy numbers above; and a production-wiring test that drives the exported
|
||
`GenerateRecoveryCode` 500×, so the fix is proven at the entry point and not only in the helper.
|
||
|
||
### Red-proof — RUN, both deterministic and probabilistic halves
|
||
|
||
Pre-fix shape restored (`var wordlist = parseWordlist(wordlistRaw)`), suite re-run:
|
||
|
||
```
|
||
--- FAIL: TestWordlistLoaded
|
||
effective wordlist should be 7772 words (7776 EFF - 4 hyphenated), got 7776
|
||
--- FAIL: TestGenerateRecoveryCode_EntropyAndFormat
|
||
joined code must segment into 10 words, got 11 (a drawn word contained "-")
|
||
--- FAIL: TestEntropyFloorSurvivesFiltering
|
||
filtered entropy moved: 129.248, expected 129.241
|
||
--- FAIL: TestGenerateRecoveryCode_NeverContainsAmbiguousWord
|
||
code 193 segmented into 11 parts, want 10
|
||
```
|
||
|
||
Fix restored → green; `git diff` clean.
|
||
|
||
`TestGeneratedCodeSegments_FilteredVsUnfiltered` is the **deterministic** companion: it drives the
|
||
generator against a fixture list where every word is hyphenated, so the pre-fix defect reproduces with
|
||
probability 1 rather than ~1/5, and shows the same list through `joinSafe` refuses to generate at all.
|
||
|
||
## Gates
|
||
|
||
`go build ./... && go vet ./... && go test ./...` — **green**. The ex-flaky package was additionally
|
||
run 10× consecutively: 10/10 pass (it previously failed ~1 in 5).
|
||
|
||
## Observations
|
||
|
||
- The `felhom-testing` skill's "Known flake … re-run before diagnosing; it is not a regression" line
|
||
is now removed. It had been actively harmful: it told every future session to dismiss a true
|
||
positive. Replaced with the generalised lesson — "known flake, just re-run it" is a diagnosis and
|
||
needs evidence like any other.
|
||
- `WordlistSize()` now reports the **effective** (filtered) draw space, 7772. `WordlistFilteredOut()`
|
||
is new, for audit. Any external consumer expecting a literal 7776 would need updating; there is none
|
||
outside the tests.
|