Files
2026-05-27 23:29:53 +01:00

59 lines
3.1 KiB
Markdown

# Change: enterprise-crypto
## Why
`@suaveplan/crypto` is the single surface every other package uses for hashing, HMAC, HKDF/PBKDF2/Argon2id key derivation, AEAD symmetric encryption, and public-key signing. Having one canonical implementation with identical semantics across Bun, Node, and browser means consumers never pick between `node:crypto` and `crypto.subtle` — they import from one place, get structured errors, constant-time comparisons, buffer zeroing, and injectable RNG/clock for tests.
Per the foundation-100-coverage rule, it ships at 100% coverage.
## What Changes
### New package: `packages/universal/crypto`
Pure-TypeScript cryptographic substrate. Runtime dependencies: `@noble/ciphers`, `@noble/curves`, `@noble/hashes` (audited pure-TS crypto libraries), plus `@suaveplan/error`, `@suaveplan/types`, `tslib`, `zod`.
#### Primitives
- **`randomBytes(n, { rng? })`** — cryptographic random bytes; default uses `crypto.getRandomValues`
- **`constantTimeEqual(a, b)`** — constant-time byte comparison
- **`zeroize(buf)`** — overwrite a `Uint8Array` with zeros
#### Hashes
- **`sha256(data)`**, **`sha384(data)`**, **`sha512(data)`** — one-shot digests returning `Uint8Array`
#### Keyed MACs + KDFs
- **`hmac(algo, key, data)`** — HMAC-SHA-256/384/512
- **`hkdf(algo, ikm, { salt, info, length })`** — HKDF extract-and-expand
- **`pbkdf2(algo, password, { salt, iterations, length })`** — PBKDF2
- **`argon2id(password, { salt, timeCost, memoryCost, parallelism, length })`** — Argon2id (memory-hard)
#### AEAD
- **`aeadEncrypt(algo, key, plaintext, { nonce, aad? })`** — AES-256-GCM and ChaCha20-Poly1305
- **`aeadDecrypt(algo, key, ciphertext, { nonce, aad? })`** — paired decrypter; throws on tag mismatch
#### Signing + key agreement
- **`ed25519.generateKeypair({ rng? })`**, **`ed25519.sign(secret, message)`**, **`ed25519.verify(public, signature, message)`**
- **`x25519.generateKeypair({ rng? })`**, **`x25519.agree(mySecret, theirPublic)`** — ECDH shared-secret
- **`ecdsaP256.generateKeypair({ rng? })`**, **`ecdsaP256.sign(secret, message)`**, **`ecdsaP256.verify(public, signature, message)`**
#### Error surface
- **`CryptoError`** extends `SuaveplanError<CryptoCode>` with codes like `CRYPTO.AEAD.DECRYPT_FAILED`, `CRYPTO.KEY.INVALID_LENGTH`, `CRYPTO.SIGN.VERIFY_FAILED`, `CRYPTO.KDF.INVALID_PARAMS`, `CRYPTO.RNG.UNAVAILABLE`
- Errors never leak plaintext, key bytes, or tag values in messages/context
#### Determinism
- All randomness flows through an injectable RNG param (`{ rng?: Rng }`) — the default is a thin wrapper over `crypto.getRandomValues`, located in exactly one file so tests can swap it
- No module-scope `Math.random()` / `Date.now()` — guarded by `tests/no-disallowed.test.ts`
## Impact
- Unblocks every downstream package that needs crypto (auth, jwt-tenant, keys, network-encryption, surrealdb keys, etc.)
- Depends on `@suaveplan/error`, `@suaveplan/types`, `@noble/ciphers`, `@noble/curves`, `@noble/hashes`
- One new catalog group (`crypto`) in the root `package.json`
- Non-goal for this change: streaming AEAD (needs `@suaveplan/streams` first), hardware-backed key storage, post-quantum algorithms, HSM integration