Files
suaveplan/openspec/archive/2026-04-19-enterprise-crypto/proposal.md
2026-05-27 23:29:53 +01:00

3.1 KiB

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