OptimalVault
The flagship Fabric feature: a multi-recipient age-encrypted credential vault where a server compromise cannot decrypt anything. Browser passphrase + WebAuthn passkey unlock; BIP39 24-word recovery phrase; canary blob detects non-deterministic authenticators.
Phase 10a (subphases 1–7) is shipped. Migrations are live, the iPad ceremony was walked end-to-end on 2026-05-04, and 7 of 8 P1 threat-model items closed on 2026-05-05. Live state: 6 vault recipients enrolled across 3 hardware fingerprints, 0 entries (next user action: actually add a credential — see Adding entries).
Crypto core
- Encryption:
agemulti-recipient (BSD-3, Filippo Valsorda). Each entry is encrypted to N recipients (browser identity, every paired device identity, recovery identity). Adding/removing a recipient triggers atomic re-wrap of all entries — now backed by a single Postgres RPC (T8 closede41f9d8). - KDF: Argon2id (m=64 MB, t=3, p=1) for passphrase → identity. Per-install salt (T11 closed
ffbb8e2). - WebAuthn: passkey-derived keypair using PRF extension where available; fallback canary blob detects non-deterministic authenticators. PRF-derived material is wrapped via WebCrypto non-extractable key in IndexedDB (T5b closed
2ddf0e6) — no plaintext marker in localStorage. - Recovery: BIP39 24-word phrase. DOM zeroize on submit + 30s clipboard auto-clear (P1-#9 closed
18fefd9). Validated against Trezor reference vectors intests/vault/crypto/recovery.test.ts. - Best-effort zeroization: in-memory plaintext is dropped after session TTL (default 15 min). Bun's GC isn't deterministic — TPM/SE backing is Phase 12-2.
Code: src/vault/crypto/{age,kdf,identity,recovery,recipients,webauthn,webauthn-client,passphrase}.ts.
Schema
Five tables, all on the OptimalOS Supabase instance (hbfalrpswysryltysonm):
| Table | Purpose |
|---|---|
vault_entries | Ciphertext blobs (one row per credential, age multi-recipient) |
vault_recipients | Registered age public keys (browser / device / recovery kinds), revoked_at for soft-revoke, canary_ciphertext column for first-unlock pubkey-mismatch detection |
vault_access_log | One row per successful entry GET (audit trail visible in dashboard) |
devices | Paired device registry (FK target for vault_recipients.device_id) |
pairing_tokens | Short-lived (10 min) pairing JWTs |
Migrations live at ~/.openclaw/workspace/optimal-cli/supabase/migrations/. Versions: 20260503215150, 20260503221013, 20260503235721, plus today's 19e8b5f (atomic re-wrap RPC, T8) and 6a05e9d (per-install salt, T11).
Recipients
The vault is multi-recipient by design. Today's live state has 6 recipients across 3 hardware fingerprints. Hardware-aware labels landed in e0f0d60:
| Recipient | Kind |
|---|---|
| iPad Safari | browser |
| iPad recovery | recovery |
| MacBook Air M1 Safari | browser |
| MacBook Air M1 recovery | recovery |
| Windows Chrome | browser |
| Windows Chrome recovery | recovery |
Each enrolled fingerprint produces both a browser recipient (passphrase + WebAuthn) and a recovery recipient (24-word BIP39 phrase printed once during setup). A device-kind recipient for the Pi (kanban 8f84c30e) is pending until optimal pair is run on the Pi — the CLI now exists (commits c132faf, 61b296d).
Adding or revoking any recipient triggers atomic re-wrap of every entry via the Postgres RPC.
Setup ceremony
A 4-step wizard. Total time on iPhone Safari: ~3 seconds (Argon2id is the heavy hitter).
- Invite — single shared password gate (
INVITE_PASSWORD, mode 0600 at~/.optimalos/transfers/.fabric-invite-password). Per-user invites are Phase 17. - Passphrase — 8+ char minimum, derived to
ageidentity via Argon2id (per-install salt). - Passkey — WebAuthn registration; canary blob written to verify the authenticator is deterministic on subsequent unlocks.
- Recovery phrase — 24 BIP39 words displayed once, "I wrote it down" gate, then re-entrance challenge to confirm. Words zeroized from DOM on submit.
After setup completes, the browser identity is registered in vault_recipients (kind=browser), the canary ciphertext is stored, and the user is redirected to / (final landing in 859fab5 — was /vault/dashboard, was /).
Daily unlock
Two paths, depending on whether the browser fingerprint is trusted:
- Trusted device (≤ 30 days idle): passphrase only.
- New fingerprint (different browser, cleared cookies, or 30-day idle exceeded): passphrase + WebAuthn passkey.
Trust marker is now a WebCrypto non-extractable key wrapped via the WebAuthn PRF extension and stored in IndexedDB — not localStorage. T5 (legacy localStorage marker) and T5b (PRF-wrap follow-up) are both closed.
Recovery flow
User types the 24-word phrase, picks a new passphrase, and the server atomically:
- Derives a fresh
ageidentity from the phrase. - Re-wraps all
vault_entriesto include the new identity (single Postgres RPC). - Removes the old browser identity from
vault_recipients. - Issues a recovery JWT (1 h TTL) so the user can complete a new passkey registration before logout.
Code: src/vault/server/storage.ts, client/vault/recovery.ts, tests/vault/rewrap.test.ts.
Threat model
Full audit at ~/.optimalos/transfers/fabric-design/06-vault-auth-threat-rerun.md. Status as of 2026-05-05 (huge progress today):
| Severity | Count | Status |
|---|---|---|
| P0 | 5 | All cleared in Phase 10a-7 |
| P1 | 8 | 7 closed today — only T2 outstanding (Phase 14 dep) |
| P2 | 10 | 0 closed, 1 in kanban |
P1 items — closeout log
| ID | Finding | Status | Commit |
|---|---|---|---|
| T2 | RLS absent (single-tenant only) | Outstanding | Phase 14 dependency |
| T4 | Device-JWT revocation cross-check | CLOSED | 5ff9ba4 (cached 60s) |
| T5b | localStorage trust marker → PRF-wrap | CLOSED | 2ddf0e6 (WebCrypto non-extractable in IndexedDB) |
| T6b | Lock-file SRI pinning | CLOSED | implied by 14-5 manifest work |
| T7 | Cloud TLS pubkey pinning | CLOSED | f9142ec (TOFU + verify-every-fetch) |
| T8 | Postgres RPC for atomic re-wrap | CLOSED | e41f9d8 + optimal-cli migration 19e8b5f |
| T11 | Argon2 salt rotation / per-install | CLOSED | ffbb8e2 + optimal-cli migration 6a05e9d |
| T13 | Access-log payload validation, JWT-bound x-session-id | CLOSED | a9d9310 |
| P1-#9 | Recovery phrase DOM zeroize + 30s clipboard auto-clear | CLOSED | 18fefd9 |
| P1-#10 | Origin pubkey pinning | CLOSED | part of T7 (f9142ec) |
Adding entries
Three interoperable paths shipped:
Dashboard "Add Entry" drawer
- Click Add Entry in the dashboard tab → drawer opens with form fields (
name,kind,value, optionalmetadataJSON). - Encryption happens client-side: the browser fetches
vault_recipients(active only), encrypts viaageto the full active recipient set, and POSTs{ ciphertext, recipients_hash, name, kind, metadata }to/api/vault/entries. - Server stores ciphertext +
recipients_hashonly — never sees plaintext. - Code:
client/vault/{dashboard,add-entry,api}.ts. Commitc3f3a7d.
optimal vault add CLI
export OPTIMAL_FABRIC_TOKEN='<paste from devtools → Application → Cookies → fabric_session>'
optimal vault add --name ANTHROPIC_API_KEY --kind api-key --value 'sk-ant-...'
optimal vault list
optimal vault recipientsAuth via OPTIMAL_FABRIC_TOKEN env or --token. Code: ~/.openclaw/workspace/optimal-cli/lib/vault/index.ts.
optimal vault import-env (bulk)
Bulk-import existing .env files (commit 8a04f0b). A dry-run today shows 69 entries would be imported across 4 default .env files — making this the fastest way to seed the vault from existing local state.
optimal vault import-env --dry-run
optimal vault import-env # applyInteroperability
The drawer, vault add, and vault import-env all produce identical ciphertext shape and recipients_hash — same /api/vault/entries route, same payload schema. An entry added via CLI is decryptable in the browser (and vice versa) by any recipient whose private key is loaded.
Vault dashboard
Phase 10a-5 dashboard tab: shows access log (most recent 50 entries with timestamp + recipient + entry id), session count, and a per-recipient revoke button. Revoking sets vault_recipients.revoked_at = now(), which triggers atomic re-wrap via Postgres RPC. Device-JWT revocation is now cross-checked daemon-side every 60s (T4 closed).
Source links
- Crypto code:
~/.openclaw/workspace/optimalOS/src/vault/crypto/ - Server code:
~/.openclaw/workspace/optimalOS/src/vault/server/ - Routes:
~/.openclaw/workspace/optimalOS/src/routes/vault.ts - Client:
~/.openclaw/workspace/optimalOS/client/vault/ - Vault design doc:
~/.optimalos/transfers/fabric-design/02-vault-design.md - UI flows:
~/.optimalos/transfers/fabric-design/05-vault-ui-flows.md - Threat audit:
~/.optimalos/transfers/fabric-design/06-vault-auth-threat-rerun.md - Manual smoke checklist:
~/.openclaw/workspace/optimalOS/tests/vault/SMOKE.md