22 KiB
vault — Vault Orchestration Layer
- Change ID: vault
- Package:
@suaveplan/vault - Status: PROPOSED
- Type: new-package
- Date: 2026-04-29
Why
The Vaulted password-manager ecosystem needs a single domain abstraction that orchestrates crypto-vault (key hierarchy), @suaveplan/crypto/keychain (keypair utilities), db-sqlite (item storage), drive (attachments), drive-sharing (share links), and vault-sync (sync engine). Today, vault-shaped helpers (sealVaultPayload, openVaultPayload, LegacyVault) live alongside primitive crypto in @suaveplan/crypto/vault — that subpath is fine for the low-level seal/open building blocks, but it does NOT provide the lifecycle, item-CRUD, attachment, share, or sync orchestration consumers need. Creating @suaveplan/vault introduces a clean orchestration layer above the existing crypto primitives; satisfies the security invariant that key material never leaves crypto-vault; and provides a clean API surface for desktop, mobile, and browser-extension consumers.
Amendment 2026-05-03: This proposal originally referenced
@suaveplan/keysas the source forVaultManager/VaultFactory. On the genesis branch that package never landed — keypair management was absorbed into@suaveplan/crypto/keychainand the legacy vault helpers into@suaveplan/crypto/vaultduring the genesis architecture redesign. References to@suaveplan/keysbelow have been amended to point at the corresponding@suaveplan/cryptosubpaths.
What changes
- ADDED:
Vaultclass —Vault.open(config, options?),unlock(password, secretKey, options?),lock() - ADDED:
OpaqueKeyHandle— wraps rawUint8Arraykeys exported bycrypto-vaultbefore crossing module boundaries - ADDED:
vault.items—create(),get(),update(),delete(),list(),search()delegated todb-sqlite - ADDED:
vault.attachments—upload(),download(),delete()delegated todrive - ADDED:
vault.shares—createLink(),revokeLink()delegated todrive-sharing - ADDED:
vault.sync—pull(),push(),sync()delegated tovault-sync - ADDED:
VaultConfig,VaultState,VaultItem,ItemFields,Attachment,ShareLink,SearchQuery,SyncResulttypes - ADDED:
VaultErrorwith structured error codes for lifecycle, item, attachment, share, and sync failures - MODIFIED:
@suaveplan/crypto/vault— the orchestration concerns previously envisioned forkeys'sVaultManager/VaultFactoryare authored fresh inside@suaveplan/vault. The existing primitives at@suaveplan/crypto/vault(sealPrivateKeyBlob,openPrivateKeyBlob,sealVaultPayload,openVaultPayload,changePassword,rotateApplicationData,LegacyVault) are unchanged;@suaveplan/vaultconsumes them. No@suaveplan/keysmigration is required because that package was never created on genesis (its responsibilities are split across@suaveplan/crypto/keychain,@suaveplan/crypto/authority, and@suaveplan/crypto/vault).
Phased delivery
- Phase 1 (this change):
vaultdefines adapter interfaces (DriveLike,DriveSharingLike,VaultSyncLike) so it compiles and tests without the realdrive,drive-sharing, andvault-syncpackages. Adapters are injected viaVaultConfig. - Phase 2 (follow-up changes): When
drive,drive-sharing, andvault-syncland,vaultswaps the adapter interfaces for real implementations. No public API changes are expected at that point.
Out of scope
- UI components or React hooks — belong in
@suaveplan/react-vault(future package) - Browser-extension-specific vault access patterns — extensions delegate crypto to the desktop app via
@suaveplan/native-messaging - Biometric unlock integration —
vault.unlockaccepts password+secretKey; biometric path wraps this in@suaveplan/biometrics - Import/export from competing password managers (1Password, Bitwarden, KeePass) — deferred to dedicated import packages
- SQLCipher native integration —
db-sqlitehandles SQLite; encryption-at-rest is the vault layer's responsibility viadrive
Gap analysis: sub-standard → enterprise
Most password-manager vault implementations fall well below enterprise standards:
| Sub-standard practice | Enterprise replacement |
|---|---|
| Mixed concerns (key generation, vault logic, and UI state in one module) | Clean separation: crypto-vault owns keys, vault owns orchestration, react-vault owns UI |
| No clear lifecycle (implicit initialization, ambiguous lock/unlock boundaries) | Explicit Vault.open(config) → unlock(password, secretKey) → lock() state machine with enforced transitions |
| Key material leaked across module boundaries as strings or plain buffers | SecureBuffer from crypto-vault never crosses the vault API boundary; only opaque handles are exchanged |
No structured error handling (generic Error with string messages) |
VaultError with structured error codes for every failure mode (lifecycle, item, attachment, share, sync) |
| Ad-hoc item encryption (some fields encrypted, others not, no versioning) | Deterministic field-level encryption: concealed fields always encrypted, schema versioned, history append-only |
| No search safety (full-text search over plaintext or unencrypted indices) | FTS5 over encrypted blind indices; concealed fields excluded from search results |
| No offline queue for mutations | Durable offline queue via vault-sync; reads continue from local snapshot when disconnected |
| Monolithic sync with no conflict resolution | Client-side CRDT merge with domain-specific strategies delegated to vault-sync |
This package migrates vault orchestration from an ad-hoc, tightly-coupled module to a rigorously designed, lifecycle-driven, enterprise-grade layer.
Security model
Threat model
| Attacker | Capability | Mitigation |
|---|---|---|
| Local attacker (malware, keylogger) | Can read process memory, intercept file system | SecureBuffer eager zeroization via fill(0) from crypto-vault; key material never leaves crypto-vault boundary; vault layer only handles opaque handles |
| Remote attacker (network MITM, compromised API) | Can intercept sync payloads, replay auth | E2EE via vault-sync (XChaCha20-Poly1305); SRP-6a authentication in api layer |
| Compromised sync server | Can read stored blobs, modify sync data | Client-side encryption; server never sees plaintext, keys, or metadata; vault orchestrates encryption before any network operation |
| Forensic recovery (stolen device, cold boot) | Can extract RAM contents post-unlock | SecureBuffer zeroization on lock; no key material persisted unencrypted; vault.lock() triggers global zeroization |
Encryption boundaries
- Plaintext lives only inside the
vaultmemory boundary during active sessions (unlocked state). - Key material lives only inside
crypto-vault;vaultwraps rawUint8Arraykeys inOpaqueKeyHandleobjects before crossing module boundaries. - Item concealed fields (passwords, PINs, secure notes) are encrypted with per-item keys derived from the hierarchical key tree.
- Attachment plaintext is ephemeral: decrypted for upload/download streaming only, then zeroized.
- Search indices use blind-index tokens (HMAC of field values) rather than plaintext to enable encrypted search.
Key hierarchy and derivation
Account Password + Secret Key + Email + Salt ──► Argon2id ──► AUK (in crypto-vault)
│
▼
HKDF-Expand ──► Personal Keyset
│
├──► HKDF ──► Vault Key (per vaultId)
│ │
│ ├──► HKDF ──► Item Key (per itemId)
│ │
│ └──► HKDF ──► Sync Encryption Key
│
└──► HKDF ──► SRP Private Key / Verifier
- AUK: Ephemeral root held only in
crypto-vault.vaultnever accesses it directly. - Vault Key: Per-vault symmetric key used to derive item keys.
- Item Key: Per-item symmetric key for encrypting concealed fields.
- Sync Encryption Key: Derived from vault key for E2EE sync payloads.
Two-secret key derivation (2SKD) threat model
vault does not perform 2SKD itself, but it enforces the security invariant that the AUK (2SKD output) never leaves crypto-vault:
vault.unlock(password, secretKey)delegates derivation tocrypto-vaultand receives only an opaque vault-key handle.- Compromise of the
vaultprocess alone does not reveal key material becausecrypto-vaultuses pinned memory and zeroization. - Compromise of both
vaultandcrypto-vaultstill requires the password and Secret Key to derive the AUK.
Secure memory
- All key material is allocated inside
crypto-vaultasUint8Arraykeys;vaultwraps them inOpaqueKeyHandleinstances before storing them. vaultstores only opaque key handles in its state machine.- On
vault.lock(), all opaque handles are invalidated andcrypto-vault.zeroizeAll()is invoked. - Attachment streaming uses fixed-size chunks; no large plaintext buffers are retained.
Side-channel resistance
- Search blind-index tokens are computed via constant-time HMAC to prevent timing leaks of field values.
- Item field comparison (e.g., duplicate detection) uses constant-time comparison for concealed fields.
- Vault state transitions (locked → unlocked) are not secret-dependent branches at the
vaultlayer.
Compliance
- NIST SP 800-63B: Relies on
crypto-vaultArgon2id for memorized secret verification; satisfies NIST-approved key hierarchy via HKDF. - OWASP Cryptographic Storage Cheat Sheet: Field-level encryption with AEAD, unique per-item keys, and secure memory satisfies recommended controls.
- GDPR Article 32: E2EE with client-side key management provides a strong technical safeguard for personal data processing.
Performance SLOs
| Metric | Target | Measurement |
|---|---|---|
| Unlock latency | P99 ≤ 1.5s on target hardware | End-to-end from vault.unlock() call to usable vault state (includes Argon2id in crypto-vault) |
| Item CRUD latency | P99 ≤ 10 ms | Create, read, update, delete a single item (includes encryption/decryption) |
| Search latency (FTS5) | P99 ≤ 50 ms for 10K items | Full-text query over 10,000 items with category/tag filters |
| Sync latency (pull+push) | P99 ≤ 500 ms for ≤ 100 changed items | End-to-end sync delegation to vault-sync |
| Attachment upload/download | P99 ≤ 200 ms per 1 MiB | Streaming encryption/decryption via drive |
| Memory bound (10K items) | ≤ 128 MiB resident | Vault state + SQLite connection + crypto-vault key hierarchy |
| Argon2id parameterization | Memory = 64 MiB, iterations = 3, parallelism = 4 | Default from crypto-vault; higher tiers configurable |
| Scalability: max vault size | 100,000 items | Hierarchical key derivation is O(1) per item; SQLite FTS5 scales with proper indexing |
| Scalability: max attachment size | 100 MiB | Streaming through drive; vault layer handles metadata only |
Observability plan
All telemetry is safe-by-construction: no key material, no item content, no plaintext fields, no vault-specific identifiers, and no user data in spans, metrics, or logs.
OTel spans and metrics
vault.unlock.duration— histogram of unlock latency (no password, no secret key info)vault.lock.duration— histogram of lock latency (includes zeroization time)vault.item.crud.duration— histogram of item CRUD latency (no item type, no count)vault.search.duration— histogram of search latency (no query terms, no result count)vault.sync.duration— histogram of sync delegation latency (no direction, no vault ID)vault.attachment.transfer.duration— histogram of attachment upload/download latency (no size, no file name)vault.state— gauge of current vault state (0 = locked,1 = unlocked)- Span names:
vault.unlock,vault.lock,vault.item.crud,vault.search,vault.sync,vault.attachment.transfer
Structured logging
Safe-to-log fields only:
event: "vault_unlocked"—durationMs,successevent: "vault_locked"—durationMs,zeroizedBufferCountevent: "item_created"—durationMs(no item type, no field names)event: "search_completed"—durationMs(no query, no result count)event: "sync_delegated"—durationMs,direction(pull/push/bidirectional)event: "attachment_transferred"—durationMs,operation(upload/download)
Error telemetry
- Aggregate error counts by
VaultErrorcode (e.g.,VAULT_LOCKED,ITEM_NOT_FOUND,SYNC_FAILED). - No vault ID, item ID, attachment name, or user context attached.
Health checks
health.vault.unlock— periodic test unlock with a synthetic config; fails if latency > 2× SLO.health.vault.crud— periodic create/get/delete of a synthetic item; fails on error or latency spike.health.vault.search— periodic FTS5 query over synthetic index; fails on error or latency spike.health.vault.lock— verifies lock triggers zeroization and state transition.
Audit logging
audit.vault.unlock—timestamp,deviceIdHash,success(no password, no key material).audit.vault.lock—timestamp,deviceIdHash,zeroizedBufferCount.audit.vault.item.access—timestamp,deviceIdHash,operation(create/read/update/delete),itemTypeHash(one-way hash of item type, not item ID).- Retention: 90 days hot, 1 year cold.
Dependency audit
| Dependency | Justification | Supply-chain risk | Tree-shakeable | License |
|---|---|---|---|---|
@suaveplan/crypto-vault |
Key hierarchy, 2SKD, SecureBuffer, zeroization |
Internal monorepo; audited in CI | Yes | MIT |
@suaveplan/crypto (subpaths /keychain, /keychain/keypair, /keychain/storage, /authority, /vault) |
Keypair generation, validation, keychain storage adapters, Authority chain-of-trust (replaces upstream ChainSigner), low-level vault seal/open primitives consumed by the orchestration layer |
Internal monorepo; audited in CI | Yes | MIT |
@suaveplan/db-sqlite |
Item storage, FTS5 search, history append | Internal monorepo | Yes | MIT |
@suaveplan/drive |
Attachment upload/download streaming | Internal monorepo | Yes | MIT |
@suaveplan/drive-sharing |
Share link creation/revocation | Internal monorepo | Yes | MIT |
@suaveplan/vault-sync |
Offline-first sync engine, CRDT merge | Internal monorepo; audited in CI | Yes | MIT |
@suaveplan/error |
Base error class | Internal monorepo | Yes | MIT |
No external runtime dependencies. All orchestration dependencies are internal monorepo packages. This eliminates hidden transitive dependencies and minimizes supply-chain attack surface.
Operational readiness
Graceful degradation
- If sync server is unreachable, the vault enters offline mode: reads and local mutations continue; sync is deferred to
vault-syncqueue. - If attachment upload fails (network or storage quota), the item is saved locally and the attachment is queued for retry.
- If search index is corrupted, the vault falls back to linear scan with a warning telemetry event and schedules index rebuild.
- If
crypto-vaultderivation fails (bad password), unlock rejects immediately withVaultError— no silent downgrades.
Resource limits
- Max items per vault: 100,000. Beyond this, creation is rejected with
VaultError. - Max attachment size: 100 MiB per file. Prevents memory exhaustion and storage abuse.
- Max concurrent attachment transfers: 3 per vault. Prevents network and memory saturation.
- Max search query length: 256 characters. Prevents pathological FTS5 queries.
- Max offline sync queue depth: 10,000 pending operations (enforced by
vault-sync).
Error recovery
- Unlock failures are non-retryable by design (bad password, bad config). Caller receives structured error code.
- Item CRUD failures trigger rollback of in-memory state; SQLite transaction ensures atomicity.
- Sync failures are retried with exponential backoff by
vault-sync(base 2, max delay 60s, max 10 retries). - Attachment transfer failures are retried up to 3 times; persistent failures queue for later retry.
- Search index corruption triggers automatic rebuild from encrypted item data on next unlock.
Backup/restore compatibility
- Vault data is backed up as standard SQLite + encrypted attachments. No proprietary formats.
- Key hierarchy is fully deterministic from mnemonic + passphrase (stored in
crypto-vault); restore re-derives identical keys. - Sync queue is serialized as standard JSON with base64-encoded ciphertext fields — portable across platforms.
- Restore replays the sync queue in causal order; merging is deterministic.
Upgrade/downgrade safety
- Vault item schema includes a version field. New versions can add optional fields; old versions ignore unknown fields.
- SQLite schema migrations are versioned and tested. Downgrade is supported to the previous minor version.
- Attachment format includes a version header. New encryption algorithms can be introduced alongside old ones.
- No key format migrations required for pure orchestration — math does not change.
Key rotation without full re-encryption
- Account password rotation: Delegated to
crypto-vault; re-derives AUK and re-encrypts personal keyset wrapper only. Vault keys and item keys remain unchanged. - Secret Key rotation: Delegated to
crypto-vault; re-derives AUK and re-encrypts personal keyset wrapper. Vault keys and item keys remain unchanged. - Vault key rotation: Delegated to
crypto-vault; derives new vault key from personal keyset. Item keys are re-encrypted under the new vault key (header-only change). - Item key rotation: Delegated to
crypto-vault; derives new item key from vault key. Re-encrypts single item only. - Sync encryption key rotation: Delegated to
vault-sync; re-encrypts pending queue entries and latest sync payload header.
Verification approach
src/vault/vault.test.ts— lifecycle (open/unlock/lock), state transitions, locked-operation rejection, re-unlock freshnesssrc/items/items.test.ts— CRUD, encryption of concealed fields, history append, tombstone deletion, validationsrc/search/search.test.ts— FTS5 delegation, category/tag/favorite filters, concealed-field exclusionsrc/attachments/attachments.test.ts— upload/download round-trip, encryption, deletion, locked-state rejectionsrc/shares/shares.test.ts— link creation/revocation, key-material exclusion, locked-state rejectionsrc/sync/sync.test.ts— pull/push/bidirectional sync delegation, offline queue, locked-state rejectionsrc/error/error-codes.test.ts— every error code is thrown by at least one production pathtests/dep-graph.test.ts— dependency invariants (no circular deps withcrypto-vault,keys,drive,drive-sharing,vault-sync)
Coverage and quality targets
- 100% coverage target: All source files must achieve 100% statement and branch coverage.
- Property-based tests:
fast-checkproperties for vault lifecycle state transitions, item CRUD round-trip, search query safety, attachment upload/download round-trip, and sync delegation correctness. - Load/stress tests: 10,000-item vault CRUD to verify memory bounds and latency SLOs; 100 concurrent unlock/lock cycles to verify zeroization and state machine robustness; 1,000 concurrent searches to verify FTS5 contention limits.
- Security audit checklist:
- Argon2id memory cost ≥ 64 MiB enforced at runtime (delegated to
crypto-vault) - All key material zeroized on lock (
vault.lock()triggerscrypto-vault.zeroizeAll()) - No plaintext key material in any log, error message, or
toString()output - Concealed fields encrypted before persistence; no plaintext passwords in SQLite
- Search blind indices do not reveal field values (HMAC with vault-scoped key)
- Locked vault rejects all item, attachment, share, and sync operations
- Vault state machine transitions are deterministic and enforced (no invalid states)
- Share links never include key material or vault identifiers
- Argon2id memory cost ≥ 64 MiB enforced at runtime (delegated to
- Penetration test scenarios:
- Dump process memory after
vault.lock()— verify no vault key, item key, or AUK material present - Attempt CRUD operation on locked vault — verify rejection with
VAULT_LOCKEDerror - Fuzz search queries — verify no crashes, no information leakage, no SQL injection
- Attempt direct SQLite access — verify concealed fields are ciphertext, not plaintext
- Crash during attachment upload — verify no plaintext in crash dump or temp files
- Dump process memory after
Migration impact
None on genesis — additive only. @suaveplan/keys was never created on genesis; its responsibilities are already split across the existing @suaveplan/crypto subpaths. The vault layer is therefore a pure new-package landing with no source moves and no deprecation aliases.
Final ownership matrix on genesis:
@suaveplan/crypto/keychain(existing) owns:generateKeypair,validateKeypair,assertValidKeypair,Keychainplus storage adapters.@suaveplan/crypto/authority(existing) owns: chain-of-trust signing — replaces upstream'sChainedKeySigner.@suaveplan/crypto/vault(existing) owns: low-level seal / open primitives (sealPrivateKeyBlob,openPrivateKeyBlob,sealVaultPayload,openVaultPayload,changePassword,rotateApplicationData,LegacyVault). Unchanged by this proposal.@suaveplan/crypto-vault(shipped) owns: BIP39, Argon2id, 2SKD, SRP-6a, key hierarchy,SecureBuffer,OpaqueKeyHandle.@suaveplan/vault(this change) owns:Vaultlifecycle (Vault.open/unlock/lock),VaultFactory, item / attachment / share / sync orchestration, encrypted-field policy,OpaqueKeyHandleboundary enforcement.