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,2 @@
schema: spec-driven
created: 2026-04-18

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 |

View File

@@ -0,0 +1,49 @@
# Change: enterprise-crypto-family
## Why
The original `enterprise-crypto` change planned a single monolithic package. After a full audit of the existing `main` branch crypto package and the downstream packages that consume it, the surface is too wide for one package: hashing/KDFs have no engine concept, AEAD/signing/agreement benefit from swappable engine implementations (noble default, libsodium for interop, WebCrypto for hardware AES in-browser), and keypair lifecycle and vault belong in their own focused packages. Splitting now, before anything ships on genesis, costs nothing — the first published version is right.
## What Changes
### New packages (6)
- **`@suaveplan/hash`** — pure-noble hashing substrate: SHA-256/384/512, HMAC, HKDF, PBKDF2, Argon2id. No engine concept. Zero WASM. Depended on by `@suaveplan/crypto` and usable standalone.
- **`@suaveplan/crypto`** — the main crypto facade. Engine interfaces (`AeadEngine`, `SignEngine`, `AgreeEngine`, `SealedBoxEngine`), noble defaults for every interface, public façade functions (`aeadEncrypt`, `aeadDecrypt`, `sign`, `verify`, `agree`, `sealedBox.*`), and primitives (`constantTimeEqual`, `randomBytes` via injectable `ByteSource`, `zeroize`). Depends on `@suaveplan/hash`.
- **`@suaveplan/crypto-sodium`** — opt-in libsodium engine. Implements every engine interface from `@suaveplan/crypto` via `libsodium-wrappers-sumo`. Adds sodium-exclusive APIs not in any interface: `cryptoKdf`, `cryptoAuth`, `cryptoPwhashStr`/`Verify`, `ristretto255`. Handles `await sodium.ready` once at import.
- **`@suaveplan/crypto-browser`** — opt-in WebCrypto engine. Implements every engine interface via `crypto.subtle`. Adds browser-exclusive APIs: `rsa.*`, `aesKw`, `ecdsaP384/P521`, `ecdhP384/P521`, `nonExtractableKey`. Hardware AES-GCM acceleration in supporting browsers.
- **`@suaveplan/crypto-keys`** — keypair lifecycle built on top of `@suaveplan/crypto`. `generateKeypair`, `validateKeypair`, keychain store, persistence adapters (in-memory, serialized). Engine-agnostic.
- **`@suaveplan/crypto-vault`** — vault and BIP39 mnemonic system. Derives keys via Argon2id/PBKDF2 (from `@suaveplan/hash`), chain-signer pattern, event emission via `@suaveplan/events`. Supersedes `@suaveplan/keys` on main.
### Supersedes
- `openspec/changes/enterprise-crypto` — the single-package plan is replaced by this family. The original `enterprise-crypto` change is archived.
### Breaking changes
None on genesis (nothing has shipped yet).
## Capabilities
### New Capabilities
- `hash-primitives`: SHA-2 digests, HMAC, HKDF, PBKDF2, Argon2id — all via `@noble/hashes`. Lives in `@suaveplan/hash`.
- `crypto-engine-interfaces`: `AeadEngine`, `SignEngine`, `AgreeEngine`, `SealedBoxEngine`, `AuthBoxEngine` — the pluggable engine contracts exported by `@suaveplan/crypto`.
- `crypto-noble-defaults`: noble implementations of every engine interface, wired as defaults in `@suaveplan/crypto`.
- `crypto-primitives`: `constantTimeEqual`, `randomBytes` (injectable `ByteSource`), `zeroize`, `Rng`→`ByteSource` rename to resolve conflict with `@suaveplan/types`'s `Rng`.
- `crypto-sodium-engine`: libsodium implementations of all engine interfaces plus sodium-exclusive primitives.
- `crypto-browser-engine`: WebCrypto implementations of all engine interfaces plus browser-exclusive primitives.
- `crypto-keys`: keypair generation, validation, and storage lifecycle.
- `crypto-vault`: mnemonic derivation, vault unlock, chain-signer, event emission.
### Modified Capabilities
None — no existing genesis specs exist for these capabilities yet.
## Impact
- **Dependencies added to catalog**: `@noble/hashes`, `@noble/ciphers`, `@noble/curves` (already planned), `libsodium-wrappers-sumo`, `@types/libsodium-wrappers-sumo`
- **Dependency order**: `hash` → `crypto` → `crypto-sodium` + `crypto-browser` (parallel) → `crypto-keys` → `crypto-vault`
- **`@suaveplan/types` `Rng` preserved** — crypto renames its own byte-producer type to `ByteSource` to eliminate the name collision without touching `@suaveplan/types`
- **Downstream unblocked**: `@suaveplan/jwt-tenant`, `@suaveplan/auth`, `@suaveplan/network` encryption, `@suaveplan/slsa`, every package that currently imports from `main`'s `@suaveplan/crypto`
- **Non-goals**: streaming AEAD (needs `@suaveplan/streams`), post-quantum algorithms, HSM/TPM, JOSE/JWT (belongs in `@suaveplan/jwt-tenant`)

View File

@@ -0,0 +1,33 @@
# Capability: crypto-browser-engine
## ADDED Requirements
### Package identity
- REQ-BRW-001: Package name `@suaveplan/crypto-browser`, `packages/universal/crypto-browser`.
- REQ-BRW-002: Runtime deps: `@suaveplan/crypto`, `@suaveplan/error`, `tslib`. Zero Node-only deps.
- REQ-BRW-003: All exports are async (return `Promise<...>`) — `crypto.subtle` is async.
### Engine implementations
- REQ-BRW-010: Exports `webCryptoAeadEngine: AeadEngine` — AES-256-GCM via `crypto.subtle.encrypt`/`decrypt` (hardware-accelerated in supporting browsers/runtimes).
- REQ-BRW-011: Exports `webCryptoSignEngine: SignEngine` — Ed25519 and ECDSA-P256 via `crypto.subtle`.
- REQ-BRW-012: Exports `webCryptoAgreeEngine: AgreeEngine` — X25519 and ECDH-P256 via `crypto.subtle`.
### Browser-exclusive primitives
- REQ-BRW-020: Exports `rsa` namespace: `{ generateKeypair(algorithm, modulusLength?), sign(algorithm, privateKey, message), verify(algorithm, publicKey, signature, message), encrypt(publicKey, plaintext), decrypt(privateKey, ciphertext) }`. Algorithms: `"RSA-PSS"`, `"RSA-OAEP"`, `"RSASSA-PKCS1-v1_5"`.
- REQ-BRW-021: Exports `aesKw` namespace: `{ wrapKey(wrappingKey, keyToWrap), unwrapKey(wrappingKey, wrappedKey) }`.
- REQ-BRW-022: Exports `ecdsaP384` namespace: sign/verify/generateKeypair on P-384 curve.
- REQ-BRW-023: Exports `ecdhP384` namespace: agree/generateKeypair on P-384 curve.
- REQ-BRW-024: Exports `nonExtractableKey(algorithm, usages): Promise<CryptoKey>` — generates a non-extractable `CryptoKey` handle. Key material is never accessible to JS code after generation.
### Availability check
- REQ-BRW-030: Exports `isWebCryptoAvailable(): boolean` — returns `typeof crypto !== 'undefined' && !!crypto.subtle`.
- REQ-BRW-031: All operations throw `BrowserCryptoError("BROWSER_CRYPTO.NOT_AVAILABLE")` if called in an environment without `crypto.subtle`.
### Error surface
- REQ-BRW-040: Exports `BrowserCryptoError extends SuaveplanError<BrowserCryptoCode>`.
- REQ-BRW-041: Codes: `BROWSER_CRYPTO.NOT_AVAILABLE`, `BROWSER_CRYPTO.KEY.GENERATION_FAILED`, `BROWSER_CRYPTO.OPERATION.FAILED`.

View File

@@ -0,0 +1,22 @@
# Capability: crypto-engine-interfaces
## ADDED Requirements
### Engine interface types
- REQ-ENG-001: `@suaveplan/crypto` exports `AeadEngine` interface: `{ encrypt(key, plaintext, nonce, aad?): Uint8Array; decrypt(key, ciphertext, nonce, aad?): Uint8Array }`.
- REQ-ENG-002: `@suaveplan/crypto` exports `SignEngine` interface: `{ generateKeypair(byteSource?): Keypair; sign(secretKey, message): Uint8Array; verify(publicKey, signature, message): boolean }`.
- REQ-ENG-003: `@suaveplan/crypto` exports `AgreeEngine` interface: `{ generateKeypair(byteSource?): Keypair; agree(mySecret, theirPublic): Uint8Array }`.
- REQ-ENG-004: `@suaveplan/crypto` exports `SealedBoxEngine` interface: `{ seal(recipientPublic, plaintext): Uint8Array; open(recipientSecret, recipientPublic, ciphertext): Uint8Array }`.
- REQ-ENG-005: `@suaveplan/crypto` exports `AuthBoxEngine` interface: `{ seal(senderSecret, recipientPublic, plaintext, nonce): Uint8Array; open(senderPublic, recipientSecret, ciphertext, nonce): Uint8Array }`.
### Algorithm type identifiers
- REQ-ENG-010: `AeadAlgorithm = "AES-256-GCM" | "ChaCha20-Poly1305" | "XChaCha20-Poly1305"`.
- REQ-ENG-011: `SignAlgorithm = "Ed25519" | "ECDSA-P256"`.
- REQ-ENG-012: `AgreeAlgorithm = "X25519" | "ECDH-P256"`.
### Engine interfaces are types only
- REQ-ENG-020: Engine interfaces are exported as TypeScript `interface` declarations — no runtime class, no factory function, no registry. Consumers receive an engine instance as a plain object.
- REQ-ENG-021: All engine interface methods that may fail throw `CryptoError` with appropriate codes.

View File

@@ -0,0 +1,32 @@
# Capability: crypto-keys
## ADDED Requirements
### Package identity
- REQ-KEYS-001: Package name `@suaveplan/crypto-keys`, `packages/universal/crypto-keys`.
- REQ-KEYS-002: Runtime deps: `@suaveplan/crypto`, `@suaveplan/error`, `@suaveplan/types`, `tslib`, `zod`.
### Keypair generation
- REQ-KEYS-010: Exports `generateKeypair(algo: SignAlgorithm | AgreeAlgorithm, opts?: { byteSource?: ByteSource; engine?: SignEngine | AgreeEngine }): Keypair`.
- REQ-KEYS-011: `Keypair = { publicKey: Uint8Array; secretKey: Uint8Array; algorithm: string }`.
- REQ-KEYS-012: Delegates to the appropriate `@suaveplan/crypto` façade based on `algo`.
### Keypair validation
- REQ-KEYS-020: Exports `validateKeypair(keypair: Keypair): ValidationResult`.
- REQ-KEYS-021: Checks: key lengths match algorithm constants, public key is derivable from secret key (for Ed25519 and X25519).
- REQ-KEYS-022: Returns `{ valid: boolean; errors: string[] }`.
### Keychain store
- REQ-KEYS-030: Exports `Keychain` class: `{ add(id: string, keypair: Keypair): void; get(id: string): Keypair | undefined; remove(id: string): void; list(): string[]; clear(): void }`.
- REQ-KEYS-031: In-memory by default. Accepts an optional `KeychainAdapter` for persistence.
- REQ-KEYS-032: Exports `KeychainAdapter` interface: `{ load(): Promise<Map<string, Keypair>>; save(entries: Map<string, Keypair>): Promise<void> }`.
- REQ-KEYS-033: Exports `MemoryKeychainAdapter` (default) and `SerializedKeychainAdapter` (serialises to/from JSON string via a provided read/write pair).
### Error surface
- REQ-KEYS-040: Exports `CryptoKeysError extends SuaveplanError<CryptoKeysCode>`.
- REQ-KEYS-041: Codes: `CRYPTO_KEYS.KEYPAIR.INVALID`, `CRYPTO_KEYS.KEYCHAIN.NOT_FOUND`, `CRYPTO_KEYS.ADAPTER.LOAD_FAILED`, `CRYPTO_KEYS.ADAPTER.SAVE_FAILED`.

View File

@@ -0,0 +1,42 @@
# Capability: crypto-noble-defaults
## ADDED Requirements
### AEAD façade
- REQ-NOBLE-010: Exports `aeadEncrypt(algo: AeadAlgorithm, key: Uint8Array, plaintext: Uint8Array, options: AeadEncryptOptions, opts?: { engine?: AeadEngine }): Uint8Array`.
- REQ-NOBLE-011: Exports `aeadDecrypt(algo: AeadAlgorithm, key: Uint8Array, ciphertext: Uint8Array, options: AeadDecryptOptions, opts?: { engine?: AeadEngine }): Uint8Array`.
- REQ-NOBLE-012: `AeadEncryptOptions = { nonce: Uint8Array; aad?: Uint8Array }`. `AeadDecryptOptions` is the same shape.
- REQ-NOBLE-013: Default engine is noble: `@noble/ciphers` AES-256-GCM and ChaCha20-Poly1305; `@noble/ciphers` XChaCha20-Poly1305.
- REQ-NOBLE-014: Key must be 32 bytes; throws `CryptoError("CRYPTO.KEY.INVALID_LENGTH")` otherwise.
- REQ-NOBLE-015: Nonce must be 12 bytes for AES-256-GCM and ChaCha20-Poly1305, 24 bytes for XChaCha20-Poly1305; throws `CryptoError("CRYPTO.AEAD.INVALID_NONCE")` otherwise.
- REQ-NOBLE-016: Exports `AEAD_KEY_LENGTH = 32`, `AEAD_NONCE_LENGTH = 12`, `AEAD_XCHACHA_NONCE_LENGTH = 24`.
### Sign façade
- REQ-NOBLE-020: Exports `ed25519` namespace: `{ generateKeypair(opts?), sign(secretKey, message), verify(publicKey, signature, message) }`.
- REQ-NOBLE-021: Exports `ecdsaP256` namespace: same shape as `ed25519`.
- REQ-NOBLE-022: All functions accept `{ engine?: SignEngine }` as a trailing options arg.
- REQ-NOBLE-023: Noble defaults: `@noble/curves` ed25519 and P-256.
- REQ-NOBLE-024: `verify` always returns `boolean`, never throws on a bad signature.
- REQ-NOBLE-025: `ecdsaP256.generateKeypair` retries until `noble.utils.isValidSecretKey(secretKey)` is true.
### Agree façade
- REQ-NOBLE-030: Exports `x25519` namespace: `{ generateKeypair(opts?), agree(mySecret, theirPublic) }`.
- REQ-NOBLE-031: Accepts `{ engine?: AgreeEngine }`.
- REQ-NOBLE-032: Noble default: `@noble/curves` x25519.
### Sealed-box façade
- REQ-NOBLE-040: Exports `sealedBox` namespace: `{ seal(recipientPublic, plaintext, opts?), open(recipientSecret, recipientPublic, ciphertext, opts?) }`.
- REQ-NOBLE-041: Noble default: X25519 key agreement + XChaCha20-Poly1305 encryption (crypto_box_seal semantics).
### Auth-box façade
- REQ-NOBLE-050: Exports `authBox` namespace: `{ seal(senderSecret, recipientPublic, plaintext, nonce, opts?), open(senderPublic, recipientSecret, ciphertext, nonce, opts?) }`.
- REQ-NOBLE-051: Noble default: X25519 key agreement + XChaCha20-Poly1305 (crypto_box semantics).
### Constants
- REQ-NOBLE-060: Exports `ED25519_SECRET_KEY_LENGTH = 32`, `ED25519_PUBLIC_KEY_LENGTH = 32`, `ED25519_SIGNATURE_LENGTH = 64`, `X25519_KEY_LENGTH = 32`, `ECDSA_P256_SIGNATURE_LENGTH = 64`.

View File

@@ -0,0 +1,33 @@
# Capability: crypto-primitives
## ADDED Requirements
### ByteSource type
- REQ-PRIM-001: `@suaveplan/crypto` exports `ByteSource = (n: number) => Uint8Array` — the byte-producing injection type used for key generation.
- REQ-PRIM-002: `Rng` is exported as a type alias for `ByteSource` for backwards compatibility during the main→genesis port. New code uses `ByteSource`.
- REQ-PRIM-003: `ByteSource` is explicitly documented as distinct from `@suaveplan/types`'s `Rng` interface (which produces numbers, not bytes).
### randomBytes
- REQ-PRIM-010: Exports `randomBytes(n: number, opts?: { byteSource?: ByteSource }): Uint8Array`.
- REQ-PRIM-011: Default `byteSource` calls `crypto.getRandomValues` — this is the **only** callsite for `crypto.getRandomValues` in the package.
- REQ-PRIM-012: Throws `CryptoError("CRYPTO.RNG.INVALID_LENGTH")` if `n` is not a non-negative integer.
- REQ-PRIM-013: Exports `defaultByteSource: ByteSource`.
### constantTimeEqual
- REQ-PRIM-020: Exports `constantTimeEqual(a: Uint8Array, b: Uint8Array): boolean`.
- REQ-PRIM-021: Uses XOR-accumulate pattern — branch-free for content bytes. Length mismatch short-circuits (length is not secret).
- REQ-PRIM-022: Returns `true` iff `a.length === b.length` and all bytes are identical.
### zeroize
- REQ-PRIM-030: Exports `zeroize(buf: Uint8Array): void` — fills buffer with zeros in-place.
- REQ-PRIM-031: Best-effort (JS GC may have copied); intent is to minimise sensitive-data lifetime on the heap.
### Error surface
- REQ-PRIM-040: Exports `CryptoError extends SuaveplanError<CryptoCode>`.
- REQ-PRIM-041: Error codes: `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`.
- REQ-PRIM-042: No error message or context field contains raw key bytes, plaintext, or authentication tags.

View File

@@ -0,0 +1,36 @@
# Capability: crypto-sodium-engine
## ADDED Requirements
### Package identity
- REQ-SOD-001: Package name `@suaveplan/crypto-sodium`, `packages/universal/crypto-sodium`.
- REQ-SOD-002: Runtime deps: `libsodium-wrappers-sumo`, `@suaveplan/crypto`, `@suaveplan/error`, `tslib`.
- REQ-SOD-003: Peer dep: `@suaveplan/crypto` at the same genesis version.
### Initialisation
- REQ-SOD-010: Exports `sodiumReady(): Promise<void>` — resolves when `libsodium-wrappers-sumo` WASM is initialised. Idempotent; resolves immediately on subsequent calls.
- REQ-SOD-011: All engine implementations call `await sodiumReady()` internally; callers do not need to call it explicitly.
### Engine implementations
- REQ-SOD-020: Exports `sodiumAeadEngine: AeadEngine` — implements AES-256-GCM (where available via hardware), ChaCha20-Poly1305, XChaCha20-Poly1305 via `libsodium`.
- REQ-SOD-021: Exports `sodiumSignEngine: SignEngine` — Ed25519 sign/verify/generateKeypair via `libsodium`.
- REQ-SOD-022: Exports `sodiumAgreeEngine: AgreeEngine` — X25519 agree/generateKeypair via `libsodium`.
- REQ-SOD-023: Exports `sodiumSealedBoxEngine: SealedBoxEngine` — `crypto_box_seal` / `crypto_box_seal_open` semantics.
- REQ-SOD-024: Exports `sodiumAuthBoxEngine: AuthBoxEngine` — `crypto_box_easy` / `crypto_box_open_easy` semantics.
### Sodium-exclusive primitives
- REQ-SOD-030: Exports `cryptoKdf(key: Uint8Array, subkeyId: number, context: string, length: number): Uint8Array` — deterministic subkey derivation via `crypto_kdf_derive_from_key`. Context must be exactly 8 bytes when encoded.
- REQ-SOD-031: Exports `cryptoAuth(key: Uint8Array, message: Uint8Array): Uint8Array` — BLAKE2b-based MAC via `crypto_auth`.
- REQ-SOD-032: Exports `cryptoAuthVerify(key: Uint8Array, message: Uint8Array, mac: Uint8Array): boolean` — verifies BLAKE2b MAC.
- REQ-SOD-033: Exports `cryptoPwhashStr(password: Uint8Array, opts?: { opsLimit?, memLimit? }): string` — Argon2id encoded hash string (includes salt, ready to store).
- REQ-SOD-034: Exports `cryptoPwhashStrVerify(hash: string, password: Uint8Array): boolean`.
- REQ-SOD-035: Exports `ristretto255` namespace: `{ add, sub, mul, fromHash, toBytes, fromBytes, random }` — group operations.
### Error surface
- REQ-SOD-040: Exports `SodiumError extends SuaveplanError<SodiumCode>`.
- REQ-SOD-041: Codes: `SODIUM.INIT.FAILED`, `SODIUM.KDF.INVALID_CONTEXT`, `SODIUM.KDF.INVALID_LENGTH`, `SODIUM.AUTH.VERIFY_FAILED`.

View File

@@ -0,0 +1,31 @@
# Capability: crypto-vault
## ADDED Requirements
### Package identity
- REQ-VAULT-001: Package name `@suaveplan/crypto-vault`, `packages/universal/crypto-vault`.
- REQ-VAULT-002: Runtime deps: `@suaveplan/crypto`, `@suaveplan/crypto-keys`, `@suaveplan/hash`, `@suaveplan/events`, `@suaveplan/error`, `@suaveplan/types`, `@scure/bip39`, `tslib`, `zod`.
### BIP39 mnemonic
- REQ-VAULT-010: Exports `generateMnemonic(wordCount?: 12 | 15 | 18 | 21 | 24, opts?: { byteSource? }): string`.
- REQ-VAULT-011: Exports `validateMnemonic(mnemonic: string): boolean`.
- REQ-VAULT-012: Exports `mnemonicToSeed(mnemonic: string, passphrase?: string): Uint8Array` — 64-byte seed via PBKDF2-SHA512 per BIP39.
### Vault lifecycle
- REQ-VAULT-020: Exports `Vault` class: `{ unlock(mnemonic, passphrase?), lock(), isLocked(): boolean, sign(keyId, message), derive(path), getPublicKey(keyId) }`.
- REQ-VAULT-021: Unlock derives a master key via Argon2id from the mnemonic seed. Locked vault holds no key material in memory.
- REQ-VAULT-022: `lock()` calls `zeroize` on all in-memory key buffers before clearing references.
- REQ-VAULT-023: Vault emits typed events via `@suaveplan/events`: `unlocked`, `locked`, `signed`, `derived`.
### Chain signer
- REQ-VAULT-030: Exports `ChainSigner` class: `{ addKey(id, keypair), removeKey(id), sign(keyId, message, opts?) }`.
- REQ-VAULT-031: Signs using the key algorithm associated with the stored keypair.
### Error surface
- REQ-VAULT-040: Exports `CryptoVaultError extends SuaveplanError<CryptoVaultCode>`.
- REQ-VAULT-041: Codes: `CRYPTO_VAULT.LOCKED`, `CRYPTO_VAULT.MNEMONIC.INVALID`, `CRYPTO_VAULT.KEY.NOT_FOUND`, `CRYPTO_VAULT.DERIVATION.FAILED`.

View File

@@ -0,0 +1,62 @@
# Capability: hash-primitives
## ADDED Requirements
### Package identity
- REQ-HASH-001: Package name is `@suaveplan/hash`, located at `packages/universal/hash`.
- REQ-HASH-002: Runtime deps: `@noble/hashes`, `@suaveplan/error`, `tslib`, `zod`. Zero browser/React/Node-only deps.
- REQ-HASH-003: Ships `"sideEffects": false`. All modules are individually subpath-exported for tree-shaking.
### Hash digests
- REQ-HASH-010: Exports `sha256(data: Uint8Array): Uint8Array` — 32-byte SHA-256 digest.
- REQ-HASH-011: Exports `sha384(data: Uint8Array): Uint8Array` — 48-byte SHA-384 digest.
- REQ-HASH-012: Exports `sha512(data: Uint8Array): Uint8Array` — 64-byte SHA-512 digest.
- REQ-HASH-013: Exports `HashAlgorithm = "SHA-256" | "SHA-384" | "SHA-512"`.
- REQ-HASH-014: Exports `hash(algo: HashAlgorithm, data: Uint8Array): Uint8Array` — algorithm dispatcher.
### HMAC
- REQ-HASH-020: Exports `hmac(algo: HashAlgorithm, key: Uint8Array, data: Uint8Array): Uint8Array`.
- REQ-HASH-021: Empty key is permitted (library-level; policy enforcement is the caller's responsibility).
### HKDF
- REQ-HASH-030: Exports `hkdf(algo: HashAlgorithm, ikm: Uint8Array, options: HkdfOptions): Uint8Array`.
- REQ-HASH-031: `HkdfOptions = { salt: Uint8Array; info: Uint8Array; length: number }`. All fields required.
- REQ-HASH-032: Throws `HashError("HASH.KDF.INVALID_PARAMS")` if `length <= 0` or `length > 255 * hashLen`.
### PBKDF2
- REQ-HASH-040: Exports `pbkdf2(algo: HashAlgorithm, password: Uint8Array, options: Pbkdf2Options): Uint8Array`.
- REQ-HASH-041: `Pbkdf2Options = { salt: Uint8Array; iterations: number; length: number }`.
- REQ-HASH-042: Throws `HashError("HASH.KDF.INVALID_PARAMS")` if `iterations < 600_000` (OWASP 2023 minimum for SHA-256). Same minimum for SHA-384/512 (conservative — these are also interactive-login strength minimums).
- REQ-HASH-043: Exports `PBKDF2_MIN_ITERATIONS = 600_000`.
### Argon2id
- REQ-HASH-050: Exports `argon2id(password: Uint8Array, options: Argon2idOptions): Uint8Array`.
- REQ-HASH-051: `Argon2idOptions = { salt: Uint8Array; timeCost: number; memoryCost: number; parallelism: number; length: number }`.
- REQ-HASH-052: Throws `HashError("HASH.KDF.INVALID_PARAMS")` per-field: `timeCost < 1`, `memoryCost < 8`, `parallelism < 1`, `length <= 0`. Each error's `context` includes `{ field, value, minimum }`.
- REQ-HASH-053: Exports `ARGON2ID_MIN_MEMORY_COST_KIB = 8`, `ARGON2ID_MIN_TIME_COST = 1`, `ARGON2ID_MIN_PARALLELISM = 1`.
### Encoding utilities
- REQ-HASH-060: Exports `toHex(bytes: Uint8Array): string` and `fromHex(hex: string): Uint8Array`.
- REQ-HASH-061: Exports `toBase64(bytes: Uint8Array): string`, `fromBase64(b64: string): Uint8Array`, `toBase64Url(bytes: Uint8Array): string`, `fromBase64Url(b64url: string): Uint8Array`.
- REQ-HASH-062: `fromHex`/`fromBase64`/`fromBase64Url` throw `HashError("HASH.ENCODING.INVALID_INPUT")` on malformed input.
### Error surface
- REQ-HASH-070: Exports `HashError extends SuaveplanError<HashCode>`.
- REQ-HASH-071: Error codes namespace: `HASH.KDF.INVALID_PARAMS`, `HASH.ALGORITHM.UNSUPPORTED`, `HASH.ENCODING.INVALID_INPUT`.
- REQ-HASH-072: No error message or context field contains raw key material or password bytes.
### Quality
- REQ-HASH-080: 100% statement + branch coverage (istanbul).
- REQ-HASH-081: fast-check property tests: determinism (same input → same output), HMAC known-vector (RFC 4231 vectors).
- REQ-HASH-082: mitata benchmarks: sha256/sha384/sha512 at 1 KiB and 1 MiB; argon2id at minimum params.
- REQ-HASH-083: Every `.ts` implementation file has a sibling `.md` (≥200 words).
- REQ-HASH-084: `tests/no-disallowed.test.ts` asserts no `Math.random`, `Date.now`, or `crypto.getRandomValues` in `src/`.

View File

@@ -0,0 +1,101 @@
# Tasks: enterprise-crypto-family
## Pre-work
- [x] 1. Archive `openspec/changes/enterprise-crypto/` → `openspec/archive/2026-04-18-enterprise-crypto/`
- [x] 2. Add catalog entries to root `package.json`: `@noble/hashes`, `@noble/ciphers`, `@noble/curves`, `libsodium-wrappers-sumo`, `@types/libsodium-wrappers-sumo`, `@scure/bip39`
- [x] 3. Delete `packages/universal/crypto/` (existing genesis scaffold, will be rebuilt as the new multi-package family)
## Wave 1 — @suaveplan/hash (no workspace deps)
- [x] 4. Scaffold `packages/universal/hash`: `package.json` (deps: `@noble/hashes`, `@suaveplan/error`, `tslib`, `zod`), `tsconfig.lib.json`, `vite.config.ts`, `vitest.config.ts` (100/100/100/100 thresholds)
- [x] 5. `errors/`: `HashError`, `HASH_CODES`, `hash-error.ts`, `codes.ts`, `index.ts`
- [x] 6. `hash/sha2.ts` + test + md: `sha256`, `sha384`, `sha512`, `hash(algo, data)`, `HashAlgorithm`
- [x] 7. `mac/hmac.ts` + test + md: `hmac(algo, key, data)`; `hasherFor` private to package
- [x] 8. `kdf/hkdf.ts` + test + md: `hkdf(algo, ikm, { salt, info, length })`
- [x] 9. `kdf/pbkdf2.ts` + test + md: `pbkdf2`, `PBKDF2_MIN_ITERATIONS`, parameter validation
- [x] 10. `kdf/argon2id.ts` + test + md: `argon2id`, min constants, per-field validation
- [x] 11. `encoding/hex.ts` + `encoding/base64.ts` + tests + mds
- [x] 12. `constants/index.ts`; barrel `src/index.ts`; subpath exports in `package.json`
- [x] 13. `tests/no-disallowed.test.ts`, `tests/tree-shake.test.ts`
- [x] 14. fast-check property tests; mitata benchmarks
- [x] 15. `README.md`
- [x] 16. `bunx turbo lint typecheck test:coverage --filter=@suaveplan/hash --force` → green at 100%
- [x] 17. `bun scripts/publish.ts --package @suaveplan/hash`
## Wave 2 — @suaveplan/crypto (depends on hash, error, types)
- [x] 18. Scaffold `packages/universal/crypto`: `package.json` (deps: `@noble/ciphers`, `@noble/curves`, `@suaveplan/hash`, `@suaveplan/error`, `@suaveplan/types`, `tslib`, `zod`), configs, 100% thresholds
- [x] 19. `errors/`: `CryptoError`, `CRYPTO_CODES`
- [x] 20. `engines/`: `aead-engine.types.ts`, `sign-engine.types.ts`, `agree-engine.types.ts`, `sealed-box-engine.types.ts`, `auth-box-engine.types.ts`
- [x] 21. `primitives/random-bytes.ts` + test + md: `ByteSource`, `defaultByteSource`, `randomBytes`; single `crypto.getRandomValues` callsite
- [x] 22. `primitives/constant-time-equal.ts` + test + md
- [x] 23. `primitives/zeroize.ts` + test + md
- [x] 24. `aead/noble-aead-engine.ts` + `aead/aead.ts` + test + md: `aeadEncrypt`, `aeadDecrypt`, `AeadAlgorithm` (adds XChaCha20-Poly1305), key/nonce validation
- [x] 25. `sign/noble-sign-engine.ts` + `sign/ed25519.ts` + test + md
- [x] 26. `sign/ecdsa-p256.ts` + test + md (retry loop for key validity)
- [x] 27. `agree/noble-agree-engine.ts` + `agree/x25519.ts` + test + md
- [x] 28. `sealed-box/noble-sealed-box-engine.ts` + `sealed-box/sealed-box.ts` + test + md
- [x] 29. `auth-box/noble-auth-box-engine.ts` + `auth-box/auth-box.ts` + test + md
- [x] 30. `constants/index.ts`; barrel `src/index.ts`; subpath exports
- [x] 31. `tests/no-disallowed.test.ts`, `tests/dep-graph.test.ts`, `tests/tree-shake.test.ts`
- [x] 32. fast-check property tests; mitata benchmarks; Zod schemas for all public option types
- [x] 33. `README.md`
- [x] 34. `bunx turbo lint typecheck test:coverage --filter=@suaveplan/crypto --force` → green at 100%
- [x] 35. `bun scripts/publish.ts --package @suaveplan/crypto`
## Wave 3 — @suaveplan/crypto-sodium and @suaveplan/crypto-browser (parallel, both depend on crypto)
- [x] 36. Scaffold `packages/universal/crypto-sodium`: package.json (deps: `libsodium-wrappers-sumo`, `@types/libsodium-wrappers-sumo`, `@suaveplan/crypto`, `@suaveplan/error`, `tslib`), configs
- [x] 37. `init.ts` + md: `sodiumReady()` lazy singleton
- [x] 38. `engines/sodium-aead-engine.ts` + test + md
- [x] 39. `engines/sodium-sign-engine.ts` + test + md
- [x] 40. `engines/sodium-agree-engine.ts` + test + md
- [x] 41. `engines/sodium-sealed-box-engine.ts` + test + md
- [x] 42. `engines/sodium-auth-box-engine.ts` + test + md
- [x] 43. `primitives/crypto-kdf.ts` + test + md
- [x] 44. `primitives/crypto-auth.ts` + test + md
- [x] 45. `primitives/crypto-pwhash.ts` + test + md
- [x] 46. `primitives/ristretto255.ts` + test + md
- [x] 47. barrel + subpath exports + README; `bunx turbo lint typecheck test:coverage --filter=@suaveplan/crypto-sodium --force` → 100%
- [x] 48. `bun scripts/publish.ts --package @suaveplan/crypto-sodium`
- [x] 49. Scaffold `packages/universal/crypto-browser`: package.json (deps: `@suaveplan/crypto`, `@suaveplan/error`, `tslib`), configs
- [x] 50. `engines/webcrypto-aead-engine.ts` + test + md
- [x] 51. `engines/webcrypto-sign-engine.ts` + test + md
- [x] 52. `engines/webcrypto-agree-engine.ts` + test + md
- [x] 53. `rsa/rsa.ts` + test + md
- [x] 54. `aes-kw/aes-kw.ts` + test + md
- [x] 55. `ecdsa-p384/` + `ecdh-p384/` + tests + mds
- [x] 56. `non-extractable-key/non-extractable-key.ts` + test + md
- [x] 57. `isWebCryptoAvailable` + `BrowserCryptoError`; barrel + subpath exports + README
- [x] 58. `bunx turbo lint typecheck test:coverage --filter=@suaveplan/crypto-browser --force` → 100%
- [x] 59. `bun scripts/publish.ts --package @suaveplan/crypto-browser`
## Wave 4 — @suaveplan/crypto-keys (depends on crypto)
- [x] 60. Scaffold `packages/universal/crypto-keys`: package.json, configs
- [x] 61. `keypair/generate.ts` + test + md
- [x] 62. `keypair/validate.ts` + test + md
- [x] 63. `keychain/keychain.ts` + test + md
- [x] 64. `keychain/adapters/memory-adapter.ts` + `serialized-adapter.ts` + tests + mds
- [x] 65. `errors/`; barrel + subpath exports + README
- [x] 66. `bunx turbo lint typecheck test:coverage --filter=@suaveplan/crypto-keys --force` → 100%
- [x] 67. `bun scripts/publish.ts --package @suaveplan/crypto-keys`
## Wave 5 — @suaveplan/crypto-vault (depends on crypto, crypto-keys, hash, events)
- [x] 68. Scaffold `packages/universal/crypto-vault`: package.json (deps: `@suaveplan/crypto`, `@suaveplan/crypto-keys`, `@suaveplan/hash`, `@suaveplan/events`, `@suaveplan/error`, `@suaveplan/types`, `@scure/bip39`, `tslib`, `zod`), configs
- [x] 69. `mnemonic/mnemonic.ts` + test + md: `generateMnemonic`, `validateMnemonic`, `mnemonicToSeed`
- [x] 70. (derivation folded into vault.ts — HKDF child key derivation inline)
- [x] 71. `vault/vault.types.ts` — `VaultEvents`, `VaultOptions`
- [x] 72. `vault/vault.ts` + test + md: unlock, lock, sign, derive, zeroize on lock
- [x] 73. `chain-signer/chain-signer.ts` + test + md
- [x] 74. `errors/`; barrel + subpath exports + README
- [x] 75. `bunx turbo lint typecheck test:coverage --filter=@suaveplan/crypto-vault --force` → 100%
- [x] 76. `bun scripts/publish.ts --package @suaveplan/crypto-vault`
## Close-out
- [x] 77. Update `CLAUDE.md` package listing for all 6 new packages
- [x] 78. Update `PACKAGES.yml` / `STRUCTURE.yml` with new packages and deps
- [x] 79. Archive `openspec/changes/enterprise-crypto-family/` → `openspec/archive/2026-04-18-enterprise-crypto-family/`