Commit Graph
3 Commits
Author SHA1 Message Date
thePR0M3TH3ANandClaude Fable 5 d040c2f5af Tier 1 hardening: no more silent hangs, no writes before consent
Reliability, which is where most of these reach users:
- The worker RPC had no message validation, no error/messageerror
  listener, no timeout, and terminate() left every pending promise
  unsettled. A malformed frame threw inside the listener (posting no
  response at all) and a dead worker stranded its callers -- the
  documented innerHTML-remount hazard manifested as a permanent freeze
  rather than an error. Both sides now validate the envelope, calls
  time out at 60s, and worker death rejects everything outstanding.
- requestNwcConnection fired no pending event, so a host using the
  RECOMMENDED hidden-mount pattern navigated a hidden element to a
  consent screen nobody could see and waited forever. Now symmetric
  with bitlogin-offer-pending.

Consent integrity:
- vaultOfferCheck published an origin rebinding to relays -- a durable
  mutation of the user's account -- from a plain host API call before
  any consent screen existed. It is read-only now, and a wallet stored
  under a DIFFERENT origin no longer reports as already-saved, so the
  binding can only change through the consented path.
- offerNwcConnection's guard sat above three awaits; two concurrent
  calls both passed it and the second dropped the first's promise. The
  slot is claimed synchronously.

Vault reads fail closed:
- listConnections threw rather than returning a relay-shaped subset
  when quorum is not met -- a short listing is indistinguishable from
  "you have no connections", and one relay should not get to say that.
- The 500-record page limit is reported (truncated) instead of silently
  deciding which connections exist.

Supply chain:
- All eight crypto dependencies pinned to exact versions (they held
  users' keys behind caret ranges), and Bitcoin Connect with them.

Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01CMr9Yt8SYmZcfgrg3CV2G1
2026-07-31 09:40:49 -04:00
thePR0M3TH3ANandClaude Fable 5 b414cc53d2 Security audit: fix 8 findings across crypto, vault, and widget
An adversarial audit (five independent reviewers over crypto/protocol,
the vault, the widget, relay flows, and doc honesty) found real bugs in
code this repo shipped. Fixed here, worst first:

CRITICAL - the widget worker ignored application_binding.origin on
vaultRevealNwc, so any caller naming a connection id could pull a
wallet credential the user granted to a DIFFERENT site. The worker now
derives its own origin (self.location) instead of trusting a payload
field -- which also closes binding forgery via vaultSaveNwc /
vaultSetBinding / vaultOfferCheck, where a host could bind another
site's wallet to itself and inherit its one-tap approval.

CRITICAL - VaultSession.destroy() left the vault root live (the root
regenerates every derived key) and, worse, left the session USABLE with
a zeroed prk: deriveRecordKey/deriveVaultSigningKey over 32 zero bytes
are deterministic and world-computable, so a stale reference could seal
a wallet secret under a key any observer can derive and report success.
Now wipes the root and fails loudly.

CRITICAL - changePassword published the tombstone that DESTROYS the old
capsule before checking that the new one reached quorum: a failed
rotation could leave neither password working, phrase-only recovery.
Reordered to fail closed with the old password intact, plus a
regression test that drives it with a partially-hostile relay set.

HIGH - enableConnectionVault never checked quorum on the recovery read,
so one stale relay could make it mint a SECOND vault root and orphan
every stored credential -- the outcome its own header calls
unforgivable.

HIGH - the rollback high-water mark used strict `<` and never compared
the event id it stored, so a same-second sibling of a revocation could
resurrect a revoked credential forever. Ties now break on event id.

HIGH - §11.10 claimed derived secrets are wiped; wipe() had two callers
and neither was on the password or recovery path. The Argon2id output,
both PRKs, and the 64-byte BIP-39 seed now die at their derivation
site.

MEDIUM - one malformed record threw out of the listing loop and made
every OTHER connection unlistable; recovery lacked the
already-registered-at-this-locator guard that registration and rotation
both carry; the recovery chain check silently passed on gaps and forks.
Synthetic (untrusted) clicks and submits no longer drive consent UI.

Docs corrected where they overstated: the README's "private keys never
leave the worker" now says plainly that a same-origin host can reach
exportIdentity, and connection-vault.md gains §19 recording six OPEN
limitations -- no CAS on capsule writes, per-device-only rollback
state, tombstones not destroying prior ciphertext, the sudo key
shipping with the root, fetches not failing closed, and a plaintext
mark store.

Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01CMr9Yt8SYmZcfgrg3CV2G1
2026-07-31 09:00:24 -04:00
thePR0M3TH3ANandClaude Fable 5 a465e1d8fa Implement the Connection Vault protocol layer (phases A-C)
connection-vault.md 5-13 as code, with the implementation-time decisions
recorded in a new section 18 and vault-ux.md holding the agreed UX
walkthroughs as the authorization layer's acceptance criteria.

Capsules gain paired optional fields connection_vault_root and
vault_sudo_key (both or neither, validated); registration mints them,
login/change-password/recovery carry them forward, and
enableConnectionVault is the phrase-gated migration for existing
accounts -- recovery capsule written first, fail closed, idempotent by
reusing roots found in either capsule so a partial failure can never
mint twice and strand records.

src/vault/ holds the finalized key hierarchy (HKDF/ScalarExpand
constants pinned by test vectors that must only ever be reverted, never
updated), per-connection encrypted records signed by a derived vault
identity with the capsule JCS+padding+AES-GCM pipeline and shared
buckets so record types are indistinguishable by size, the NWC profile
with lossless URI parse/export, relay sync reusing the publish-and-
verify quorum bar, and per-record high-water marks so a stale replica
is withheld and reported rather than silently resurrecting a revoked
credential.

The two-tier model makes sudo mode cryptographically real: personal-
tier record keys require vault_sudo_key as HKDF input, VaultSession
holds it only inside an enableSudo/endSudo window (wiping on the way in
and out), and outside a window personal records are invisible -- the
same trial-decrypt returns null, not an error. The app-facing API is
structurally blind to the tier by construction.

118 tests passing, including four end-to-end scenarios against mock
relays: clean-device restore, password-change carry, legacy-account
migration (wrong phrase refused), and rollback detection.

Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01CMr9Yt8SYmZcfgrg3CV2G1
2026-07-30 12:29:46 -04:00