# Design: enterprise-crypto-family ## Context The existing `enterprise-crypto` change planned a single `@suaveplan/crypto` package covering hashing, KDFs, AEAD, signing, and key agreement. After auditing the `main` branch implementation (all source files, not just exports) and the downstream packages that consume it, the design splits into six focused packages. All six ship at `0.1.0` on genesis. The noble libraries (`@noble/hashes`, `@noble/ciphers`, `@noble/curves`) remain the default implementation everywhere — zero WASM, audited, tree-shakeable, identical across Bun/Node/browser. The sodium and browser packages are opt-in overlays, not replacements. A name collision was discovered: `@suaveplan/crypto` currently exports `Rng = (n: number) => Uint8Array` while `@suaveplan/types` exports `Rng = { nextUint32(): number; nextFloat(): number }`. This change renames crypto's type to `ByteSource` throughout. ## Goals / Non-Goals **Goals:** - Ship six focused packages in dependency order: `hash` → `crypto` → (`crypto-sodium` ∥ `crypto-browser`) → `crypto-keys` → `crypto-vault` - Preserve every public name from the `main` branch `@suaveplan/crypto` — nothing is removed, some things move packages - Engine-pluggable AEAD, Sign, Agree, SealedBox, AuthBox via interface + noble default in `@suaveplan/crypto`; sodium/browser are opt-in engines - 100% coverage on all six packages (Layer 1/2 foundation rule) - Full enterprise checklist: Zod schemas, structured errors, fast-check property tests, mitata benchmarks, OTEL instrumentation, co-located `.md` docs **Non-Goals:** - Streaming AEAD — waits on `@suaveplan/streams` - Post-quantum (Kyber, Dilithium) — revisit when @noble ships stable implementations - HSM / TPM / hardware-backed key storage - JOSE / JWT / JWE — belongs in `@suaveplan/jwt-tenant` - Replacing `@suaveplan/testing`'s inline `seededRng` — the testing rule forbids adding workspace deps ## Decisions ### Package: `@suaveplan/hash` Standalone hashing substrate, no engine concept needed — output is always deterministic for a given input. ```text src/ hash/ sha2.ts / sha2.test.ts / sha2.md # sha256, sha384, sha512, hash(algo, data) mac/ hmac.ts / hmac.test.ts / hmac.md # hmac(algo, key, data) kdf/ hkdf.ts / hkdf.test.ts / hkdf.md # hkdf(algo, ikm, { salt, info, length }) pbkdf2.ts / pbkdf2.test.ts / pbkdf2.md # pbkdf2(algo, pw, { salt, iterations, length }) argon2id.ts / argon2id.test.ts / argon2id.md # argon2id(pw, { salt, timeCost, memoryCost, parallelism, length }) encoding/ hex.ts / hex.test.ts / hex.md # toHex, fromHex base64.ts / base64.test.ts / base64.md # toBase64, fromBase64, toBase64Url, fromBase64Url errors/ codes.ts / hash-error.ts / index.ts constants/index.ts index.ts ``` `HashAlgorithm = "SHA-256" | "SHA-384" | "SHA-512"` — the dispatcher type stays here. `hasherFor(algo)` is package-private (not exported), used internally by hmac/hkdf/pbkdf2. PBKDF2 enforces OWASP 2023 minimum (600,000 iterations for SHA-256). Argon2id enforces RFC 9106 §4 minimums per-field with individual error context. ### Package: `@suaveplan/crypto` Engine interfaces + noble defaults + public façade functions + primitives. ```text src/ engines/ aead-engine.types.ts # AeadEngine interface sign-engine.types.ts # SignEngine interface agree-engine.types.ts # AgreeEngine interface sealed-box-engine.types.ts # SealedBoxEngine interface auth-box-engine.types.ts # AuthBoxEngine interface aead/ aead.ts / aead.test.ts / aead.md # aeadEncrypt, aeadDecrypt, AeadAlgorithm (adds XChaCha20-Poly1305) noble-aead-engine.ts # noble default AeadEngine impl sign/ ed25519.ts / ed25519.test.ts / ed25519.md ecdsa-p256.ts / ecdsa-p256.test.ts / ecdsa-p256.md noble-sign-engine.ts agree/ x25519.ts / x25519.test.ts / x25519.md noble-agree-engine.ts sealed-box/ sealed-box.ts / sealed-box.test.ts / sealed-box.md noble-sealed-box-engine.ts auth-box/ auth-box.ts / auth-box.test.ts / auth-box.md noble-auth-box-engine.ts primitives/ random-bytes.ts / random-bytes.test.ts / random-bytes.md # ByteSource, defaultByteSource, randomBytes constant-time-equal.ts / ... zeroize.ts / ... errors/ codes.ts / crypto-error.ts / index.ts constants/index.ts index.ts ``` `AeadAlgorithm` widens to `"AES-256-GCM" | "ChaCha20-Poly1305" | "XChaCha20-Poly1305"`. `ByteSource = (n: number) => Uint8Array` — replaces the old `Rng` type. All key-generation functions accept `{ byteSource?: ByteSource }` (alias `{ rng?: ByteSource }` for backwards compat). Every public function accepts `{ engine?: }` — omit to use the noble default. Engine interfaces are exported as TypeScript types only — no runtime registration/factory needed. Consumers pass an engine instance. ### Package: `@suaveplan/crypto-sodium` Wraps `libsodium-wrappers-sumo`. Handles `await sodium.ready` via a lazy singleton that resolves once on first import. ```text src/ engines/ sodium-aead-engine.ts / ... sodium-sign-engine.ts / ... sodium-agree-engine.ts / ... sodium-sealed-box-engine.ts / ... sodium-auth-box-engine.ts / ... primitives/ crypto-kdf.ts / ... # crypto_kdf_derive_from_key — not in any interface crypto-auth.ts / ... # BLAKE2b MAC — not in any interface crypto-pwhash.ts / ... # encoded password hash string — not in any interface ristretto255.ts / ... # group ops — not in any interface init.ts / init.md # sodiumReady(): Promise errors/ index.ts index.ts ``` All exports are async-capable since sodium requires init. The engine implementations return `Promise`-wrapped results where needed, conforming to the async variant of each engine interface. ### Package: `@suaveplan/crypto-browser` Uses `crypto.subtle`. Browser and modern Node/Bun compatible (Web Crypto API). ```text src/ engines/ webcrypto-aead-engine.ts / ... # AES-256-GCM via crypto.subtle (hardware-accelerated) webcrypto-sign-engine.ts / ... # Ed25519, ECDSA-P256 via crypto.subtle webcrypto-agree-engine.ts / ... # X25519, ECDH-P256 via crypto.subtle rsa/ rsa.ts / rsa.test.ts / rsa.md # RSA-PSS, RSA-OAEP, RSASSA-PKCS1 aes-kw/ aes-kw.ts / ... # AES Key Wrap ecdsa-p384/ ecdsa-p384.ts / ... ecdh-p384/ ecdh-p384.ts / ... non-extractable-key/ non-extractable-key.ts / ... # opaque CryptoKey handle errors/ index.ts index.ts ``` All `crypto.subtle` operations are async — engine methods return `Promise`. Façade functions in `@suaveplan/crypto` already accept `engine?`; async engines are valid engine implementations. ### Package: `@suaveplan/crypto-keys` ```text src/ keypair/ generate.ts / ... # generateKeypair(algo, { byteSource?, engine? }) validate.ts / ... # validateKeypair(keypair) keychain/ keychain.ts / ... # in-memory keychain store adapters/ memory-adapter.ts / ... serialized-adapter.ts / ... errors/ index.ts index.ts ``` Engine-agnostic — delegates to `@suaveplan/crypto` façade functions. ### Package: `@suaveplan/crypto-vault` ```text src/ mnemonic/ bip39.ts / ... # generateMnemonic, mnemonicToSeed (uses @scure/bip39) vault/ vault.ts / ... # unlock(mnemonic, passphrase), lock(), sign(), derive() vault-events.ts / ... # typed events: unlocked, locked, signed derivation/ key-derivation.ts / ... # Argon2id/PBKDF2 unlock key from passphrase errors/ index.ts index.ts ``` Depends on `@suaveplan/hash` (KDFs), `@suaveplan/crypto` (signing façade), `@suaveplan/crypto-keys`, `@suaveplan/events`. ### `ByteSource` vs `Rng` — the rename `@suaveplan/types`'s `Rng = { nextUint32(): number; nextFloat(): number }` is the canonical interface for number-producing randomness (used by `@suaveplan/pseudo-random` and `@suaveplan/random`). `@suaveplan/crypto`'s byte-producer type is renamed `ByteSource = (n: number) => Uint8Array`. Both the old name (`Rng`) and new name (`ByteSource`) are exported from `@suaveplan/crypto` during genesis (no consumer on genesis yet, so no migration cost — but the alias is there for when `main` consumers are ported). ### Error shapes Every package extends `SuaveplanError` with its own stable code namespace: - `HASH.*` — `HASH.ALGORITHM.UNSUPPORTED`, `HASH.KDF.INVALID_PARAMS` - `CRYPTO.*` — `CRYPTO.AEAD.DECRYPT_FAILED`, `CRYPTO.AEAD.INVALID_NONCE`, `CRYPTO.KEY.INVALID_LENGTH`, `CRYPTO.SIGN.VERIFY_FAILED`, `CRYPTO.SIGN.INVALID_KEY`, `CRYPTO.RNG.INVALID_LENGTH`, `CRYPTO.RNG.UNAVAILABLE` - `SODIUM.*` — `SODIUM.INIT.FAILED`, `SODIUM.KDF.INVALID_CONTEXT` - `BROWSER_CRYPTO.*` — `BROWSER_CRYPTO.KEY.GENERATION_FAILED`, `BROWSER_CRYPTO.NOT_AVAILABLE` - `CRYPTO_KEYS.*`, `CRYPTO_VAULT.*` Error context never contains raw key bytes, plaintext, or authentication tags. ### Coverage strategy All six packages target 100% (Layer 1/2 foundation rule). - Determinism: same inputs → same outputs - Round-trip: encrypt→decrypt, sign→verify - Rejection: invalid lengths, sub-minimum KDF params, bad nonce sizes - Engine substitution: every public function tested with both noble default and a stub engine - Property-based (fast-check): AEAD round-trip, sign/verify identity, constant-time-equal reflexivity - Cross-cutting `tests/no-disallowed.test.ts`: guards `Math.random`, `Date.now`, raw `crypto.getRandomValues` outside `primitives/random-bytes.ts` ## Risks / Trade-offs | Risk | Mitigation | |---|---| | `libsodium-wrappers-sumo` WASM init is async | Lazy singleton in `init.ts`; tests use `await sodiumReady()` in `beforeAll` | | `crypto.subtle` async engine breaks sync call sites | Engine interfaces define both sync and async variants; noble default is sync; browser engine is async; façade returns `Promise` when engine is async | | `@scure/bip39` adds a new catalog dep | Adds to `crypto` catalog group in root `package.json`; acceptable | | Six packages is a large wave | Implement in strict dependency order; each package's pre-publish gate must be green before the next starts |