Files
bitlogin/.semgrep.yml
T
thePR0M3TH3ANandClaude Opus 5 98b2967116 Ban Math.random package-wide and enforce it in CI
Neither remaining Math.random call was a secret. The backup-quiz picker
chooses which three recovery words to re-type -- an attacker who cannot see
the phrase gains nothing from predicting them -- and the relay subscription
id is a public wire identifier scoped to one socket, where collision is the
only failure mode.

They are changed anyway, because the point is the rule rather than these two
call sites. A Math.random ban that carries a standing exception is worth
much less than one that does not: the exception is what a future misuse
hides behind, and reviewers stop reading a warning they have already agreed
to ignore twice. Both now use the CSPRNG (randomUniformInt, and
bytesToHex(randomBytes(8)) for the subscription id, same 16-hex-character
shape as before), which costs nothing here.

With no exception left, the rule can be absolute. .semgrep.yml adds three
rules over packages/*/src/**, run in CI as a separate step after
--config auto:

- bitlogin-no-math-random: no Math.random anywhere, secret or not
- bitlogin-no-crypto-fallback: no `?? fallback`, `|| fallback`, or
  catch-and-substitute around getRandomValues -- the COLDCARD defect class
- bitlogin-no-time-or-id-seeded-secrets: no hashing Date.now() or
  performance.now() into a key

Verified the rules fire rather than merely passing, which is the failure
mode of a lint rule nobody tests: reintroducing the old
Math.floor(Math.random() * pool.length) produces 1 blocking finding and exit
1, and the tree is clean at 0 findings with it restored. Path patterns are
anchored (/packages/...) so semgrep's upcoming Semgrepignore v2 change does
not silently alter their scope.

Typecheck and the full suite pass (185 tests).

This closes the entropy audit for this repo. The only gate remaining is an
independent review of the RNG path by someone other than the author.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01HtNMsKzzFTPURemX7tT19u
2026-07-31 17:30:24 -04:00

57 lines
2.1 KiB
YAML

# Local semgrep rules, run alongside `--config auto` in
# .github/workflows/security.yml.
#
# Checklist §13 of docs/entropy-audit-2026-07-31.md: make the randomness rules
# machine-enforced rather than review-enforced. Every rule here is currently
# clean across packages/ — they exist to keep it that way, not to flag a
# backlog.
rules:
- id: bitlogin-no-math-random
languages: [typescript, javascript]
severity: ERROR
message: >-
Math.random() is not a CSPRNG and must not be used anywhere in this
package, including for values that are not secrets. Use randomBytes or
randomUniformInt from @bitlogin/core/crypto. A ban with a standing
exception is where the next real misuse hides -- if you believe this
specific call is safe, the correct move is still to use the CSPRNG, which
costs nothing here.
paths:
include:
- /packages/*/src/**
pattern: Math.random(...)
- id: bitlogin-no-crypto-fallback
languages: [typescript, javascript]
severity: ERROR
message: >-
Do not fall back to another source when getRandomValues is unavailable.
Key generation must fail closed -- "continue with reduced security" is the
COLDCARD defect class. crypto/random.ts throws by design; keep it that way.
paths:
include:
- /packages/*/src/**
patterns:
- pattern-either:
- pattern: $C.getRandomValues(...) ?? $FALLBACK
- pattern: $C.getRandomValues(...) || $FALLBACK
- pattern: |
try { ... $C.getRandomValues(...) ... } catch(...) { ... return $FALLBACK; ... }
- id: bitlogin-no-time-or-id-seeded-secrets
languages: [typescript, javascript]
severity: ERROR
message: >-
Timestamps, counters and identifiers are not entropy. A secret derived
from Date.now() or performance.now() is predictable to anyone who knows
roughly when it was created.
paths:
include:
- /packages/*/src/**
patterns:
- pattern-either:
- pattern: |
$KEY = $HASH(Date.now(), ...)
- pattern: |
$KEY = $HASH(performance.now(), ...)