# 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 `Uint8Array`s that survive garbage collection without zeroization. - The AUK (Account Unlock Key) is ephemeral: computed on unlock, zeroized on lock. ### Key hierarchy and derivation ```text 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 `SecureBuffer`s 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.