17 KiB
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:
SrpClientclass — full SRP-6a client (salt, private key, verifier, ephemeral keypair, session key, server proof verification) - ADDED:
SecureBuffer—Uint8Array-based secure buffer with eager zeroization viafill(0)on close,toStringmasking, and clone isolation - ADDED:
zeroizeAll()— global zeroization of all live secure buffers - ADDED:
CryptoVaultErrorwith 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
SecureBufferinstances within thecrypto-vaultmemory boundary. - Keys live only inside
SecureBufferinstances 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:
- Something you know (password) — vulnerable to phishing, keylogging, brute-force.
- 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: UsesUint8Arraywith eager zeroization viafill(0)on close; memory locking viamlockis deferred to a future native binding.- Clone isolation: Every clone creates a distinct allocation; zeroizing one does not affect siblings.
toStringmasking: Returns[SecureBuffer]instead of hex/base64 to prevent accidental logging.- Global
zeroizeAll(): Emergency zeroization of all liveSecureBuffers 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 timecrypto-vault.secure-buffer.count— gauge of live SecureBuffers (count only, no addresses)crypto-vault.zeroize.duration— histogram of zeroization latencycrypto-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 saltevent: "vault_key_derived"—vaultIdHash: sha256(vaultId)(one-way), no keyevent: "secure_buffer_zeroized"—bufferCount: numberevent: "srp_handshake_complete"—success: boolean,durationMs: number
Error telemetry
- Aggregate error counts by
CryptoVaultErrorcode (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. SecureBufferalways uses standardUint8Arraywith eager zeroization viafill(0)on close; a warning telemetry event is emitted if a future nativemlockbinding 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/apigateway 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/biometricsand platform-specific packages - Argon2id WASM or native bindings — pure-TS/Bun implementation using
@suaveplan/crypto-hashprimitives
Verification approach
src/bip39/bip39.test.ts— BIP39 generation, round-trip, checksum validation, error pathssrc/argon2id/argon2id.test.ts— Argon2id derivation, parameter safety, salt uniquenesssrc/hd-key/hd-key.test.ts— HD master key derivation, passphrase sensitivitysrc/twoskd/twoskd.test.ts— 2SKD determinism, factor separation, empty-input rejectionsrc/key-hierarchy/key-hierarchy.test.ts— hierarchical derivation determinism, branch isolationsrc/srp/srp-client.test.ts— full handshake, wrong-password failure, tampered proof rejectionsrc/secure/secure-buffer.test.ts— zeroization, masking, clone isolationsrc/error/error-codes.test.ts— every error code is thrown by at least one production pathtests/dep-graph.test.ts— pins zero@suaveplan/*runtime deps fortestingpackages
Coverage and quality targets
- 100% coverage target: All source files must achieve 100% statement and branch coverage.
- Property-based tests:
fast-checkproperties 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-bufferzeroize()) - 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
- Dump process memory after
Impact
Existing consumers of @suaveplan/crypto-vault (v0.0.0) will be affected as follows:
- The current
vaultandchain-signersubpath 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.