Commit Graph
2 Commits
Author SHA1 Message Date
thePR0M3TH3ANandClaude Fable 5 6eb1d7d640 Pin the derivation chain, and stop Dependabot grouping crypto majors
A Dependabot PR failed the build by bundling @noble/hashes 1.8->2.2,
@noble/curves 1.9->2.2, @scure/base 1.2->2.2, @scure/bip39 1.6->2.2,
TypeScript 5->7, Vite 6->8 and Vitest 3->4 into a single change. Production
was never affected -- only that preview -- and the cause is real rather than
flaky: noble v2 exports only ".js"-suffixed subpaths, while this code imports
the extensionless "@noble/hashes/sha2" that 1.8 provides.

Failing to compile was the LUCKY outcome. The tests could not have caught the
dangerous version of this. Account capsules live on public relays, and the
login name plus password derive the locator, signing scalar and capsule key
deterministically -- so those outputs are a compatibility contract with every
account that already exists. The crypto suite only checked self-consistency
(same input, same output within one run), which stays green even if HKDF,
base64url framing, BIP-39 seeding or JCS ordering all change. A grouped major
that compiled would have shipped and locked every existing user out, with no
server-side record to migrate them from.

So this adds knownAnswer.test.ts, pinning the actual outputs of HKDF-Extract,
HKDF-Expand, ScalarExpand (scalar, counter and resulting public key), BIP-39
phrase and seed, base64url, and JCS ordering. The vectors are captured from
the shipping implementation, and the header says plainly what a failure means:
not "update the expected value", but "this upgrade is account-breaking".

dependabot.yml now groups only minor and patch updates. Majors are still
proposed, individually, so each can be judged on its own. Majors of the
derivation chain itself (@noble/*, @scure/*, hash-wasm, canonicalize) are
ignored entirely -- that is deliberate work with the vectors green, not an
automated PR waiting to be merged on a quiet afternoon.

172/172 tests, typecheck clean.

Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01CMr9Yt8SYmZcfgrg3CV2G1
2026-07-31 14:46:46 -04:00
thePR0M3TH3ANandClaude Fable 5 7d2209d706 Second-audit hardening: phrase-gated sudo key, deterministic migration, policy tests
Authored by ChatGPT/Codex in a separate review pass. Reviewed and verified
here before committing rather than taken on trust; findings below.

The substantive change is that the vault sudo key now lives ONLY in the
phrase-gated recovery capsule. A newly written password credential capsule
carries just connection_vault_root, so an ordinary login restores
connectable-tier records without putting personal-tier material behind a
password capsule that is downloadable and therefore offline-guessable forever.
Pre-hardening capsules stay readable, password login does not return their
sudo key, and the next rotation scrubs it. That closes the gate this project
had explicitly set on personal-tier UI.

Legacy vault-enable migration now derives roots deterministically from
phrase-protected material and account_id, so two concurrent attempts cannot
mint divergent roots -- the failure mode the docs previously mitigated with
"enable the vault once, from one device".

Manual (user-chosen) passwords are REMOVED, not merely discouraged, and a new
securityPolicy.test.ts pins that along with several things tests do not
usually reach: the deployment CSP (default-src 'none', frame-ancestors
'none') plus six baseline headers, demo pages free of inline scripts and
remote fonts, and README documenting same-origin self-hosting only rather
than a remote demo embed. llms.txt was updated to match, which I checked
because that test only reads README.

What I verified, since a passing suite proves less than it appears to:

- All 12 fixes from the 2026-07-30 audit survived the rewrite of recover.ts,
  changePassword.ts, validation.ts, relay.ts, session.ts and element.ts.
  Checked semantically where grep cannot help: changePassword still checks
  publish success BEFORE building the tombstone, and vaultSetBinding still
  unbinds on origin === null rather than silently rebinding to the worker
  global.
- No dependency changes; the package.json diff is scripts only.
- Argon2id and KDF parameters unchanged. The NIP-44 diff ADDS a base64 length
  cap before decoding, a memory-DoS guard.
- Validation increased rather than decreased: 44 throws added, 33 removed.
- No disabled or skipped tests, no secrets in the diff.

typecheck clean, 165/165 tests, build green, and the account manager exercised
in a browser: creation correctly refused when relay quorum could not be met,
and on retry failed at the recovery capsule with "No login credential was
published" -- the §18.1 fail-closed ordering doing its job against a flaky
public relay set.

Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01CMr9Yt8SYmZcfgrg3CV2G1
2026-07-31 13:54:36 -04:00