Initial Commit
This commit is contained in:
58
openspec/archive/2026-04-19-enterprise-crypto/proposal.md
Normal file
58
openspec/archive/2026-04-19-enterprise-crypto/proposal.md
Normal file
@@ -0,0 +1,58 @@
|
||||
# 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
|
||||
Reference in New Issue
Block a user