Pin fail-closed RNG behaviour and randomUniformInt's uniformity

crypto/random.ts is the single chokepoint for every secret this codebase
mints -- recovery entropy, everyday keys, vault roots, AEAD nonces, account
ids, generated passphrases. It is correct today: it throws when no secure
source exists rather than falling back, and rejection-samples to avoid modulo
bias. Nothing in CI would have reported it if either stopped being true.

That is the gap this closes. The existing suite is strong on known-answer
vectors -- HKDF, ScalarExpand, Argon2id, BIP-39, Schnorr, GCM tamper
detection, NIP-44 official vectors -- but every one of them is satisfied by a
deterministic stream. Same input, same output. That is exactly how a
predictable RNG survives a health check that only asks whether the output
looks random.

Adds two blocks, nine tests:

- Fail-closed: no crypto object, crypto without getRandomValues, and
  getRandomValues present but not callable must each throw, and
  randomEntropy128 / generatePrivateKey / randomUniformInt must abort rather
  than substitute a value. Plus a check that normal operation resumes once a
  source is available again, so the guard cannot be satisfied by breaking
  everything permanently.
- Uniformity: chi-square over 200k draws at modulus 94, full-range coverage
  at the EFF wordlist modulus of 7776, rejection of invalid bounds, and the
  degenerate modulus of 1.

Both mutation-verified rather than assumed. Replacing the throw with a
zero-filling stub turns five fail-closed tests red. Replacing rejection
sampling with a bare `value % maxExclusive` gives chi-square 5614 against a
threshold of 147 -- while leaving every other test in the suite green, which
is the whole reason the distribution test has to exist.

Tests only; no source or behaviour change. Suite 172 -> 181.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01HtNMsKzzFTPURemX7tT19u
This commit is contained in:
thePR0M3TH3ANandClaude Opus 5 committed 2026-07-31 16:29:21 -04:00
1 parent ba49673ef9
commit b85e1572ea
3 files changed
+120 -4

No files matched your search

+5 -3
View File
@@ -9,9 +9,11 @@ randomness-integration audit. **No predictable-fallback defect:** `random.ts`
is the single chokepoint, throws rather than degrading, and rejection-samples to
avoid modulo bias. Three hardening items, in order:
- **M2** — nothing tests that the RNG fails closed. Do this first; it is
additive, cannot touch the derivation chain, and it is what stops M1's fix
from regressing later.
- **M2 — done.** Nine tests in `packages/core/src/crypto/crypto.test.ts` pin
fail-closed behaviour and `randomUniformInt`'s uniformity. Mutation-verified:
a zero-filling stub in place of the throw turns five of them red, and
swapping rejection sampling for bare modulo gives chi-square 5614 against a
threshold of 147.
- **M1** — `nonceOverride` / `ivOverride` are reachable from the public
`@bitlogin/core/crypto` surface. Move behind a non-exported test module.
- **L1** — `Math.random` in the backup-quiz word picker (`element.ts:1471`).
+17 -1
View File
@@ -81,7 +81,23 @@ rule in `TODO.md`, that means the change is account-breaking.
---
### M2 — Nothing tests that the RNG fails closed
### M2 — Nothing tests that the RNG fails closed — **DONE 2026-07-31**
Landed in `packages/core/src/crypto/crypto.test.ts` as two new blocks, nine
tests, bringing that file to 59 and the suite to 181.
Both guards were mutation-verified rather than assumed:
| Mutation | Result |
| --- | --- |
| `webcrypto()` returns a zero-filling stub instead of throwing | 5 fail-closed tests red |
| `randomUniformInt` uses bare `value % maxExclusive` | chi-square **5614** against a threshold of 147 |
The second number is the point of the exercise: a modulo-biased generator
still produces different-looking values every call and passes every
self-consistency test in the suite. Only a distribution test sees it.
The original problem statement follows.
This is the gap the whole exercise turns on. The crypto suite is genuinely good
— known-answer coverage for HKDF, ScalarExpand, the Argon2id profile, BIP-39