Files
suaveplan/openspec/archive/2026-05-03-crypto-vault/proposal.md
2026-05-27 23:29:53 +01:00

17 KiB
Raw Blame History

crypto-vault — Cryptographic Foundation Layer

  • Change ID: crypto-vault
  • Package: @suaveplan/crypto-vault
  • Status: PROPOSED
  • Type: migration-redesign
  • Date: 2026-04-29

Why

The Vaulted password-manager ecosystem requires a dedicated cryptographic foundation that @suaveplan/keys (keypair management) and @suaveplan/crypto-* (primitives) do not provide. crypto-vault fills the gap between raw crypto primitives and vault orchestration: BIP39 mnemonic generation, Argon2id HD key derivation, two-secret key derivation (2SKD), SRP-6a authentication, and a hierarchical key path (AUK → personal keyset → vault key → item key). Without this package, vault key management would be scattered across keys and vault, violating the invariant that key material never leaves its memory boundary.

What changes

  • ADDED: generateMnemonic(strength?) / entropyToMnemonic / mnemonicToEntropy — BIP39 English word-list support
  • ADDED: deriveKeyWithArgon2id(password, salt, params) — Argon2id with OWASP-safe minimum parameter enforcement
  • ADDED: deriveMasterKeyFromMnemonic(mnemonic, passphrase?) — BIP39 seed + BIP32-like HD derivation
  • ADDED: deriveAuk(password, secretKey, email, salt, params) — 2SKD combining password + Secret Key + email
  • ADDED: derivePersonalKeyset(auk) / deriveVaultKey(personalKeyset, vaultId) / deriveItemKey(vaultKey, itemId) — hierarchical HKDF key tree
  • ADDED: SrpClient class — full SRP-6a client (salt, private key, verifier, ephemeral keypair, session key, server proof verification)
  • ADDED: SecureBuffer — Uint8Array-based secure buffer with eager zeroization via fill(0) on close, toString masking, and clone isolation
  • ADDED: zeroizeAll() — global zeroization of all live secure buffers
  • ADDED: CryptoVaultError with structured error codes for every failure mode

Current-state audit

Package exists at packages/crypto/crypto-vault (v0.0.0) with mnemonic, vault, chain-signer, errors modules. This change restructures and expands the package.

Gap analysis: sub-standard → enterprise

Most password-manager cryptographic implementations fall well below enterprise standards:

Sub-standard practice Enterprise replacement
PBKDF2 with low iteration counts (often < 100k) Argon2id with OWASP-safe minimum parameters (≥ 64 MiB memory, ≥ 3 iterations, parallelism ≥ 1)
Single-factor key derivation (password only) Two-secret key derivation (2SKD): password + Secret Key + email salt
No constant-time comparison of MACs or passwords Constant-time timingSafeEqual for all sensitive comparisons
Key material scattered across modules, leaked in strings SecureBuffer with eager zeroization via fill(0), and masked toString
Flat key hierarchy (one key encrypts everything) Hierarchical HKDF tree: AUK → personal keyset → vault key → item key
No secure memory zeroization on lock Global zeroizeAll() + per-buffer zeroize() with clone isolation
Mnemonic generation without checksum validation Full BIP39 English word-list support with checksum validation
Weak or non-standard authentication protocols SRP-6a with full client-side proof verification and tamper rejection

This package migrates the entire cryptographic foundation from ad-hoc, copy-pasted primitives to a rigorously designed, audited, and tested enterprise-grade layer.

Security model

Threat model

Attacker Capability Mitigation
Local attacker (malware, keylogger) Can read process memory, intercept system calls SecureBuffer eager zeroization via fill(0); key material never leaves crypto-vault boundary; zeroization on lock
Remote attacker (network MITM, compromised API) Can intercept sync payloads, replay auth SRP-6a mutual auth; E2EE via XChaCha20-Poly1305 handled in vault-sync
Compromised sync server Can read stored blobs, modify sync data Client-side encryption; server never sees plaintext, keys, or SRP verifier entropy
Forensic recovery (stolen device, cold boot) Can extract RAM contents post-unlock SecureBuffer zeroization on lock; no key material persisted unencrypted; Argon2id slow brute-force

Encryption boundaries

  • Plaintext lives only inside SecureBuffer instances within the crypto-vault memory boundary.
  • Keys live only inside SecureBuffer instances derived from the hierarchical tree.
  • No key material is serialized to JSON, strings, or plain Uint8Arrays that survive garbage collection without zeroization.
  • The AUK (Account Unlock Key) is ephemeral: computed on unlock, zeroized on lock.

Key hierarchy and derivation

Account Password + Secret Key + Email + Salt ──► Argon2id ──► AUK
                                                                  │
                                                                  ▼
                                              HKDF-Expand ──► Personal Keyset
                                                                  │
                                                                  ├──► HKDF ──► Vault Key (per vaultId)
                                                                  │                    │
                                                                  │                    └──► HKDF ──► Item Key (per itemId)
                                                                  │
                                                                  └──► HKDF ──► SRP Private Key / Verifier
  • AUK: Ephemeral root derived from 2SKD. Never persisted.
  • Personal Keyset: Long-lived HKDF-derived branch for vault and auth sub-keys.
  • Vault Key: Per-vault symmetric key for vault-level encryption.
  • Item Key: Per-item symmetric key for item-level encryption.

Two-secret key derivation (2SKD) threat model

2SKD splits the entropy requirement across two factors:

  1. Something you know (password) — vulnerable to phishing, keylogging, brute-force.
  2. Something you have (Secret Key) — a high-entropy device-local or paper key.

An attacker must compromise both factors to derive the AUK. Compromise of the password alone is insufficient because Argon2id parameters make brute-force infeasible. Compromise of the Secret Key alone is insufficient because it is high-entropy and useless without the password and email salt. The email salt binds the derivation to the account identity, preventing cross-account pre-computation.

Secure memory

  • SecureBuffer: Uses Uint8Array with eager zeroization via fill(0) on close; memory locking via mlock is deferred to a future native binding.
  • Clone isolation: Every clone creates a distinct allocation; zeroizing one does not affect siblings.
  • toString masking: Returns [SecureBuffer] instead of hex/base64 to prevent accidental logging.
  • Global zeroizeAll(): Emergency zeroization of all live SecureBuffers on lock, crash, or security event.

Side-channel resistance

  • Constant-time comparison: All MAC verification, password comparison, and SRP proof checks use timingSafeEqual.
  • Timing-safe MAC: HMAC-SHA256 and Poly1305 tag verification are constant-time.
  • Argon2id memory-hardness: Memory-access pattern is data-dependent, mitigating cache-timing attacks.

Compliance

  • NIST SP 800-63B: Memorized secret verifier requirements (salted, iterated, memory-hard). Argon2id satisfies the latest NIST guidance for password hashing.
  • OWASP Password Storage Guidelines: Recommends Argon2id as the primary choice. Minimum parameters enforced at runtime (memory ≥ 64 MiB, iterations ≥ 3).

Performance SLOs

Metric Target Measurement
Unlock latency (Argon2id) P99 ≤ 1.5s on target hardware End-to-end from password entry to decrypted vault key
HD key derivation (AUK → item key) P99 ≤ 10 ms Single HKDF-expand per key level
SecureBuffer allocation P99 ≤ 1 ms Pinned memory allocation + zero-fill
Zeroization latency P99 ≤ 5 ms Global zeroize of 1,000 live buffers
Memory bound (10K items) ≤ 128 MiB resident All live SecureBuffers + key hierarchy
Argon2id parameterization Memory = 64 MiB, iterations = 3, parallelism = 4 Default; higher tiers configurable
Scalability: max vault size 100,000 items Hierarchical key derivation is O(1) per item
Scalability: max attachment size 100 MiB Encryption streaming handled by drive layer; keys are fixed-size

Observability plan

All telemetry is safe-by-construction: no key material, no plaintext, no vault metadata, no item counts, and no user-identifiable data in spans, metrics, or logs.

OTel spans and metrics

  • crypto-vault.derive.argon2id.duration — histogram of Argon2id derivation time (no params, no salt)
  • crypto-vault.derive.hkdf.duration — histogram of HKDF derivation time
  • crypto-vault.secure-buffer.count — gauge of live SecureBuffers (count only, no addresses)
  • crypto-vault.zeroize.duration — histogram of zeroization latency
  • crypto-vault.srp.handshake.duration — histogram of SRP client handshake time
  • Span names: derive.deriveAuk, derive.deriveVaultKey, srp.handshake, securebuffer.allocate

Structured logging

Safe-to-log fields only:

  • event: "auk_derived" — no password, no secret key, no salt
  • event: "vault_key_derived" — vaultIdHash: sha256(vaultId) (one-way), no key
  • event: "secure_buffer_zeroized" — bufferCount: number
  • event: "srp_handshake_complete" — success: boolean, durationMs: number

Error telemetry

  • Aggregate error counts by CryptoVaultError code (e.g., ARGON2ID_PARAMS_UNSAFE, SRP_PROOF_MISMATCH).
  • No vault ID, item ID, salt, or user context attached.

Health checks

  • health.crypto-vault.derivation — periodic Argon2id derivation of a test vector; fails if latency > 2× SLO.
  • health.crypto-vault.memory — verifies SecureBuffer allocation and zeroization succeed.

Audit logging

  • audit.crypto-vault.unlock — timestamp, deviceIdHash, success (no password, no key material).
  • audit.crypto-vault.lock — timestamp, deviceIdHash, zeroizedBufferCount.
  • Retention: 90 days hot, 1 year cold.

Dependency audit

Dependency Justification Supply-chain risk Tree-shakeable License
@suaveplan/crypto-hash Argon2id and HKDF primitives Internal monorepo; audited in CI Yes MIT
@suaveplan/random CSPRNG for salts and ephemeral keys Internal monorepo; audited in CI Yes MIT
@suaveplan/encoding Base64/hex for test vectors and interop Internal monorepo Yes MIT
@suaveplan/error Base error class Internal monorepo Yes MIT

No external runtime dependencies. All primitives are either internal packages or standard Web Crypto / Node.js crypto APIs. This minimizes supply-chain attack surface and eliminates hidden transitive dependencies.

Operational readiness

Graceful degradation

  • If Argon2id parameter validation fails (e.g., caller supplies unsafe params), derivation rejects immediately with CryptoVaultError — no silent downgrades.
  • SecureBuffer always uses standard Uint8Array with eager zeroization via fill(0) on close; a warning telemetry event is emitted if a future native mlock binding is unavailable.

Resource limits

  • Argon2id max memory: 1 GiB (enforced at runtime; reject params exceeding this).
  • Max concurrent derivation operations: 4 (prevents memory exhaustion from parallel unlocks).
  • SecureBuffer max size: 64 KiB (keys and secrets only; large data handled by streaming layers).

Error recovery

  • Derivation failures are non-retryable by design (bad password, bad params). Caller receives structured error code.
  • SRP handshake failures are non-retryable beyond standard TCP retry; caller must re-initiate.
  • SecureBuffer allocation failures trigger global zeroization and a fatal health-check failure.

Backup/restore compatibility

  • Mnemonic generation produces standard BIP39 seed phrases — compatible with any BIP39 wallet or backup tool.
  • Key hierarchy is fully deterministic from mnemonic + passphrase; restore re-derives identical keys.
  • No proprietary key formats; all serialized data uses standard encoding.

Upgrade/downgrade safety

  • Argon2id parameters are versioned in the salt/header. New versions can increase costs; old versions can still derive.
  • HKDF info strings include a version prefix ("v1/...") for forward-compatible derivation paths.
  • No schema migrations required for pure crypto — math does not change.

Key rotation without full re-encryption

  • Account password rotation: Re-derive AUK with new password; re-encrypt personal keyset wrapper only. Vault keys and item keys remain unchanged.
  • Secret Key rotation: Requires re-derivation of AUK and re-encryption of personal keyset wrapper. Vault keys and item keys remain unchanged.
  • Vault key rotation: Derive new vault key from existing personal keyset; re-encrypt vault header only. Item keys remain unchanged.
  • Item key rotation: Derive new item key from existing vault key; re-encrypt single item only.

Out of scope

  • Server-side SRP-6a verifier storage or session management — belongs in @suaveplan/api gateway layer
  • BIP39 word lists other than English — defer until i18n requirement is concrete
  • Hardware security module (HSM) or secure-enclave key storage — deferred to @suaveplan/biometrics and platform-specific packages
  • Argon2id WASM or native bindings — pure-TS/Bun implementation using @suaveplan/crypto-hash primitives

Verification approach

  • src/bip39/bip39.test.ts — BIP39 generation, round-trip, checksum validation, error paths
  • src/argon2id/argon2id.test.ts — Argon2id derivation, parameter safety, salt uniqueness
  • src/hd-key/hd-key.test.ts — HD master key derivation, passphrase sensitivity
  • src/twoskd/twoskd.test.ts — 2SKD determinism, factor separation, empty-input rejection
  • src/key-hierarchy/key-hierarchy.test.ts — hierarchical derivation determinism, branch isolation
  • src/srp/srp-client.test.ts — full handshake, wrong-password failure, tampered proof rejection
  • src/secure/secure-buffer.test.ts — zeroization, masking, clone isolation
  • src/error/error-codes.test.ts — every error code is thrown by at least one production path
  • tests/dep-graph.test.ts — pins zero @suaveplan/* runtime deps for testing packages

Coverage and quality targets

  • 100% coverage target: All source files must achieve 100% statement and branch coverage.
  • Property-based tests: fast-check properties for BIP39 round-trip, Argon2id parameter enforcement, 2SKD commutativity, HKDF branch isolation, and SRP handshake correctness.
  • Load/stress tests: 1,000 concurrent Argon2id derivations to verify memory bounds and contention; 10,000 hierarchical key derivations to verify latency SLOs.
  • Security audit checklist:
    • Argon2id memory cost ≥ 64 MiB enforced at runtime
    • Argon2id iteration count ≥ 3 enforced at runtime
    • All key material zeroized on lock (zeroizeAll() + per-buffer zeroize())
    • No plaintext key material in any log, error message, or toString() output
    • All MAC and comparison operations use constant-time timingSafeEqual
    • SRP-6a server proof verified before session key use
    • SecureBuffer clone isolation verified (zeroize one, verify sibling intact)
    • Salt uniqueness verified across 10,000 generations (collision probability < 2⁻¹²⁸)
  • Penetration test scenarios:
    • Dump process memory after vault.lock() — verify no AUK, vault key, or item key material present
    • Fuzz Argon2id parameters — verify rejection of all unsafe combinations
    • Fuzz SRP handshake messages — verify no crashes or incorrect proof acceptance
    • Attempt timing attack on MAC comparison — verify distribution is statistically uniform

Impact

Existing consumers of @suaveplan/crypto-vault (v0.0.0) will be affected as follows:

  • The current vault and chain-signer subpath exports will be restructured to align with the new hierarchy.
  • New subpath exports (bip39, argon2id, twoskd, srp, secure) will be added; existing exports remain available during the migration window.
  • Internal APIs may change shape as the package moves from a narrow mnemonic+vault utility to a full cryptographic foundation.
  • All downstream packages (vault, vault-sync, keys) must update their imports to use the new module layout.

Migration impact

VaultManager and VaultFactory consumers in @suaveplan/keys migrate to @suaveplan/vault. mnemonic and chain-signer consumers update imports to the new subpath layout. A deprecation window of one minor version is provided for removed exports.