Initial Commit

This commit is contained in:
2026-05-27 23:29:53 +01:00
commit fbc70f96e6
10000 changed files with 1102152 additions and 0 deletions

View File

@@ -0,0 +1,220 @@
# 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?: <EngineType> }` — 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<void>
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<Uint8Array>`. 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 |