Files
2026-05-27 23:29:53 +01:00

22 KiB
Raw Permalink Blame History

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/keys as the source for VaultManager / VaultFactory. On the genesis branch that package never landed — keypair management was absorbed into @suaveplan/crypto/keychain and the legacy vault helpers into @suaveplan/crypto/vault during the genesis architecture redesign. References to @suaveplan/keys below have been amended to point at the corresponding @suaveplan/crypto subpaths.

What changes

  • ADDED: Vault class — Vault.open(config, options?), unlock(password, secretKey, options?), lock()
  • ADDED: OpaqueKeyHandle — wraps raw Uint8Array keys exported by crypto-vault before crossing module boundaries
  • ADDED: vault.items — create(), get(), update(), delete(), list(), search() delegated to db-sqlite
  • ADDED: vault.attachments — upload(), download(), delete() delegated to drive
  • ADDED: vault.shares — createLink(), revokeLink() delegated to drive-sharing
  • ADDED: vault.sync — pull(), push(), sync() delegated to vault-sync
  • ADDED: VaultConfig, VaultState, VaultItem, ItemFields, Attachment, ShareLink, SearchQuery, SyncResult types
  • ADDED: VaultError with structured error codes for lifecycle, item, attachment, share, and sync failures
  • MODIFIED: @suaveplan/crypto/vault — the orchestration concerns previously envisioned for keys's VaultManager / VaultFactory are authored fresh inside @suaveplan/vault. The existing primitives at @suaveplan/crypto/vault (sealPrivateKeyBlob, openPrivateKeyBlob, sealVaultPayload, openVaultPayload, changePassword, rotateApplicationData, LegacyVault) are unchanged; @suaveplan/vault consumes them. No @suaveplan/keys migration 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): vault defines adapter interfaces (DriveLike, DriveSharingLike, VaultSyncLike) so it compiles and tests without the real drive, drive-sharing, and vault-sync packages. Adapters are injected via VaultConfig.
  • Phase 2 (follow-up changes): When drive, drive-sharing, and vault-sync land, vault swaps 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.unlock accepts 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-sqlite handles SQLite; encryption-at-rest is the vault layer's responsibility via drive

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 vault memory boundary during active sessions (unlocked state).
  • Key material lives only inside crypto-vault; vault wraps raw Uint8Array keys in OpaqueKeyHandle objects 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. vault never 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 to crypto-vault and receives only an opaque vault-key handle.
  • Compromise of the vault process alone does not reveal key material because crypto-vault uses pinned memory and zeroization.
  • Compromise of both vault and crypto-vault still requires the password and Secret Key to derive the AUK.

Secure memory

  • All key material is allocated inside crypto-vault as Uint8Array keys; vault wraps them in OpaqueKeyHandle instances before storing them.
  • vault stores only opaque key handles in its state machine.
  • On vault.lock(), all opaque handles are invalidated and crypto-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 vault layer.

Compliance

  • NIST SP 800-63B: Relies on crypto-vault Argon2id 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, success
  • event: "vault_locked" — durationMs, zeroizedBufferCount
  • event: "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 VaultError code (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-sync queue.
  • 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-vault derivation fails (bad password), unlock rejects immediately with VaultError — 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 freshness
  • src/items/items.test.ts — CRUD, encryption of concealed fields, history append, tombstone deletion, validation
  • src/search/search.test.ts — FTS5 delegation, category/tag/favorite filters, concealed-field exclusion
  • src/attachments/attachments.test.ts — upload/download round-trip, encryption, deletion, locked-state rejection
  • src/shares/shares.test.ts — link creation/revocation, key-material exclusion, locked-state rejection
  • src/sync/sync.test.ts — pull/push/bidirectional sync delegation, offline queue, locked-state rejection
  • src/error/error-codes.test.ts — every error code is thrown by at least one production path
  • tests/dep-graph.test.ts — dependency invariants (no circular deps with crypto-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-check properties 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() triggers crypto-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
  • 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_LOCKED error
    • 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

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, Keychain plus storage adapters.
  • @suaveplan/crypto/authority (existing) owns: chain-of-trust signing — replaces upstream's ChainedKeySigner.
  • @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: Vault lifecycle (Vault.open / unlock / lock), VaultFactory, item / attachment / share / sync orchestration, encrypted-field policy, OpaqueKeyHandle boundary enforcement.