262 lines
17 KiB
Markdown
262 lines
17 KiB
Markdown
# 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.
|