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
This commit is contained in:
thePR0M3TH3ANandClaude Fable 5 committed 2026-07-30 12:29:46 -04:00
1 parent e9a5a41ff7
commit a465e1d8fa
22 files changed
+1843 -12

No files matched your search

+10 -5
View File
@@ -33,14 +33,19 @@ identity and recovery model.
## Connection Vault
The Connection Vault is an experimental design for portable application
credentials. It deliberately stores mutable credentials outside the core
identity capsules as independently encrypted records.
The Connection Vault stores portable application credentials outside the core
identity capsules as independently encrypted records. Its protocol layer
(records, key hierarchy, relay sync, migration, tiers) is **implemented in
`@bitlogin/core`'s `src/vault/`**; the widget authorization layer (consent
UI, grants, Connected apps) is still to come.
- [`connection-vault.md`](./connection-vault.md) — overall vault architecture,
key hierarchy, relay storage, authorization, recovery, and deletion
key hierarchy, relay storage, authorization, recovery, and deletion; §18
records the implementation-time decisions (tier model, sudo key)
- [`vault-ux.md`](./vault-ux.md) — the agreed user-experience walkthroughs,
serving as the authorization layer's acceptance criteria
- [`nwc-connections.md`](./nwc-connections.md) — Nostr Wallet Connect credential
profile
profile (parser/exporter implemented in `src/vault/nwc.ts`)
- [`s3c-format.md`](./s3c-format.md) — portable `s3c1:` format for S3-compatible
storage connections
+93 -4
View File
@@ -1,9 +1,18 @@
# BitLogin Connection Vault
**Status:** Experimental design
**Document version:** 0.1
**Date:** July 22, 2026
**Implementation status:** Not implemented
**Status:** Design with core implementation
**Document version:** 0.2
**Date:** July 30, 2026
**Implementation status:** §5–§11 and §13 are **implemented in `@bitlogin/core`**
(`src/vault/`, plus the capsule fields in `src/capsules/` and the migration in
`src/account/enableVault.ts`), with test vectors pinned in
`src/vault/vault.test.ts` and protocol round trips in `vault.e2e.test.ts`.
The application-authorization layer (§12: consent UI, grants, Connected-apps
management) is **not yet implemented** — it is the widget generation's work.
See §18 for the decisions finalized at implementation time, including the
**sensitivity-tier model and the sudo key**, which supersede this document
where they differ. The companion `vault-ux.md` holds the user-experience
walkthroughs that act as Phase D's acceptance criteria.
## 1. Summary
@@ -525,3 +534,83 @@ At minimum:
- Clearly distinguish BitLogin record deletion from provider-side revocation.
- Assume a malicious or compromised client can steal all secrets available in
that session.
## 18. Implementation-time decisions (2026-07-30, v0.2)
These were finalized when §5–§13 shipped in `@bitlogin/core`. Where this
section and the earlier prose disagree, this section wins.
### 18.1 Two capsule fields, not one
The capsules gained **two** paired 32-byte roots, not just the vault root of
§5.2:
```json
{
"connection_vault_root": "base64url-32-bytes",
"vault_sudo_key": "base64url-32-bytes"
}
```
Validation enforces that they appear together or not at all (a capsule with
one but not the other is corrupt and must not grant a session). Registration
mints both for every new account; `enableConnectionVault` is the §5.3
ceremony for accounts that predate them, requires the recovery phrase,
writes the recovery capsule first, fails closed, and is idempotent — a retry
after a partial failure REUSES roots found in either capsule rather than
minting twice, because minting twice would strand existing records.
### 18.2 The sensitivity-tier model and why the sudo key exists
Records carry a `tier` field:
- **`connectable`** (NWC, scoped S3): record keys derive from
`connection_vault_root` alone. The root is cacheable for the session, so
daily wallet use is silent. These are the records the §12 grant flow may
expose to application origins. The real security boundary is provider-side
policy — wallet budgets, scoped IAM keys — and import UIs must push users
toward it.
- **`personal`** (stored passwords, secure notes): record keys additionally
require `vault_sudo_key` as HKDF input
(`personal_prk = HKDF-Extract(SHA256("bitlogin/connection-vault-personal/v1"), root || sudo_key)`).
The honest-client contract: the sudo key is never persisted outside the
capsules and is held in memory only during a sudo window
(`VaultSession.enableSudo`/`endSudo`, which wipe it). Re-opening a window
requires re-decrypting a capsule — a fresh Argon2id run on the password, or
the phrase — which is what makes "re-enter your password to reveal" a real
key ceremony rather than the cosmetic gate §SF2 forbids.
**The app-facing API must be structurally blind to this tier**: an embedded
widget cannot do exact-origin matching, and revealing one site's password
to another site's page is a phishing machine with a consent screen.
Password-manager UX (autofill, origin matching) waits for the extension
generation. Crypto seed phrases remain banned from the vault entirely.
Tiers are invisible on the wire: identical envelopes, buckets, and d-tag
shapes, decryption is trial-based. A distinguishable ciphertext would tell
every relay which records are "the sensitive ones".
### 18.3 Resolved open decisions (§16)
1. **Capsule schema:** additive optional fields on the v1 schemas (§18.1) —
a new schema id would lock old clients out of login entirely.
**Mixed-version caveat:** an OLD client's password change rebuilds the
credential capsule without the vault fields and strands connectable
records until the next phrase ceremony repairs them. Deployments must
update vendored widgets before enabling the vault for users.
2. **Padding buckets:** the capsule buckets (1024/2048/4096) are reused.
Every current profile fixture fits 1024 with room, and a shared bucket
set keeps NWC and S3 records indistinguishable by size.
3. **Relay policy:** vault records publish to the account's configured vault
relays with the same publish-and-verify quorum bar as capsules
(`publishConnectionRecord` / `fetchConnectionRecordEvents`).
4. **App binding:** `application_binding.origin` is implemented;
`app_pubkey` remains reserved.
5. **Encrypted index:** not built; discovery lists the derived identity's
kind-30078 events and filters the `bitlogin:connection:` prefix locally.
6. **Grants:** deferred to the widget authorization layer (Phase D).
7. **S3C repository split:** deferred until a second implementation exists.
Rollback resistance (§9) is per record: a local high-water mark stores the
newest accepted `created_at` per connection id, and `fetchConnectionRecordEvents`
withholds and reports anything older instead of serving it — a stale replica
must never silently resurrect a revoked credential.
+99
View File
@@ -0,0 +1,99 @@
# Connection Vault — UX walkthroughs
**Status:** Agreed product direction (2026-07-30)
**Role:** These scenarios are the acceptance criteria for the Phase D
authorization layer (`connection-vault.md` §12, §18). The design principle
running through all of them: **friction appears exactly where danger is, and
nowhere else.** Count the friction across the whole set: one phrase entry
ever, one budget-facing tap per app, one tap per new device, sudo or phone
approval only at genuinely dangerous reveals. Everything else is silence.
## 1. Turning the vault on — existing account
Account manager card: *"Connection Vault — store wallet and storage
connections once, use them on every device."* Enabling asks for the recovery
phrase with the reason stated plainly (*"the vault must survive both your
password and your phrase"*), rewrites both capsules with quorum readback, and
fails closed with a retry that reuses the same roots
(`enableConnectionVault`). New accounts skip this scenario entirely — the
roots are minted at registration.
*Deliberate friction:* the phrase entry, once per account ever.
## 2. Connecting a wallet the first time — inside an app
The app calls the request API with a reason string. The sheet walks the user
through pasting/scanning an NWC URI, parses it, and shows what it learned —
provider, relay, and **the budget in large type**. No budget draws an amber
warning recommending a budgeted connection, never a hard block. Label
pre-filled from the app name. The final line before reveal is honest:
*"[App] will receive this connection and can spend within its budget until
you revoke it."* Save binds the record to the requesting origin.
*Deliberate friction:* one explicit share tap, budget staring at the user.
*Removed friction:* naming, binding, storage.
## 3. The payoff — new phone
Sign into the app's BitLogin, and at the first payment moment:
> **Use "Satisfied spending wallet"?**
> *You connected this wallet to Satisfied on another device. Budget: 500 sats/day.*
One tap. Zero pasting. This screen is the entire reason the vault exists.
A new device seeing a credential for the first time must never happen
silently, even with a durable grant.
## 4. Every ordinary day after
Nothing. No prompt, no branding. The grant is durable per origin per device;
the vault's best UX is absence.
## 5. Connected apps — management
Account manager page listing origin → connection → grant date → devices, with
**Revoke access** per row and one **"Revoke all app access"** panic button.
Each connection's detail page separates two red actions honestly:
- **Remove from BitLogin** — deletes the stored copy; the credential itself
keeps working for anyone who already holds it.
- **Revoke at wallet/provider** — an instruction with a guide, not a button
pretending BitLogin can reach into the wallet (§11).
## 6. S3 connections
Same sheet pattern; the import screen teaches while it collects: scoped IAM
key vs root key question, a recommended object-prefix field, amber warning on
unscoped credentials. Reveal to an app requires sudo mode — S3 secrets guard
buckets, not a 500-sat budget.
## 7. Personal tier — passwords and secure notes
Exists ONLY in the first-party account manager. No app can request, list, or
learn these records exist (the tier is invisible to the app-facing API by
construction — see §18.2). Viewing requires sudo mode: re-enter the account
password, ~5-minute window. Copy button, auto-clearing field. The copy is
honest: a safe place for a few important secrets, not a password manager —
autofill and site-matching wait for the extension generation. Seed phrases
stay banned.
## 8. Phone approval (after §SF5–7 ships)
High-tier reveals push to the enrolled phone; the record key is 2-of-2
derived so a laptop cryptographically cannot proceed on its own. Budgeted NWC
connections deliberately do NOT phone-prompt — the budget is already the
lock, and prompting every payment trains reflexive approval.
## 9. Disaster — phrase-only recovery
Twelve words on a clean device restore identity, then the vault roots ride
the recovery capsule (§13.2), so connections reappear: wallets, buckets,
passwords. The phrase remains the ungated master path (§SF9). The flow nudges
re-enrolling a phone and rotating anything sensitive.
## Build order for Phase D
The screens, in dependency order: request/consent sheet (2, 3) → guided NWC
import (2) → Connected apps + connection detail (5) → S3 import (6) → sudo
mode (7). The personal tier and phone approval layer on later without
reshaping anything before them.