## ADDED Requirements ### Requirement: Single canonical implementation The package SHALL implement every primitive on top of `@noble/ciphers`, `@noble/curves`, and `@noble/hashes` — NOT on top of `node:crypto` or `crypto.subtle`. Behaviour MUST be identical across Bun, Node, and modern browsers. #### Scenario: Same output across runtimes - **WHEN** `sha256(x)` runs under Bun AND under a browser with the same input - **THEN** the returned bytes are equal ### Requirement: Constant-time comparison The package SHALL export `constantTimeEqual(a, b)` that returns `true` iff `a` and `b` have the same length AND every byte is equal. Execution time MUST be independent of the position of the first differing byte. #### Scenario: Equal buffers - **WHEN** `constantTimeEqual(a, b)` is called with byte-equal inputs - **THEN** it returns `true` #### Scenario: Different length rejected in constant time - **WHEN** `constantTimeEqual(a, b)` is called with unequal-length inputs - **THEN** it returns `false` without branching on content ### Requirement: Zeroization The package SHALL export `zeroize(buf)` that fills `buf` with zero bytes. #### Scenario: In-place zero - **WHEN** `zeroize(x)` is called on a non-empty `Uint8Array` - **THEN** every element of `x` is `0` afterwards ### Requirement: Hash surface The package SHALL export `sha256`, `sha384`, `sha512` one-shot digest functions each returning a `Uint8Array` of the correct length (32, 48, 64 bytes respectively). #### Scenario: Round-trip determinism - **WHEN** `sha256(x)` is called twice with identical input - **THEN** both calls return byte-identical output ### Requirement: HMAC, HKDF, PBKDF2, Argon2id The package SHALL export `hmac`, `hkdf`, `pbkdf2`, `argon2id` with the algorithm selected by a string parameter for HMAC/HKDF/PBKDF2 (`"SHA-256" | "SHA-384" | "SHA-512"`). Argon2id is parameterised by `timeCost`, `memoryCost` (KiB), `parallelism`, and `length`. #### Scenario: HMAC determinism - **WHEN** `hmac("SHA-256", key, data)` is called twice with identical inputs - **THEN** both calls return byte-identical output #### Scenario: HKDF output length honoured - **WHEN** `hkdf("SHA-256", ikm, { salt, info, length: 42 })` is called - **THEN** the returned array has byte length 42 #### Scenario: PBKDF2 iterations below floor rejected - **WHEN** `pbkdf2(algo, password, { iterations: 999, ... })` is called - **THEN** it throws `CryptoError("CRYPTO.KDF.INVALID_PARAMS")` #### Scenario: Argon2id parameter validation - **WHEN** `argon2id(password, { memoryCost: 512, ... })` is called with `memoryCost < 8` - **THEN** it throws `CryptoError("CRYPTO.KDF.INVALID_PARAMS")` (Argon2id rfc 9106 minimum) ### Requirement: AEAD (AES-256-GCM, ChaCha20-Poly1305) The package SHALL export `aeadEncrypt` and `aeadDecrypt` dispatching by algorithm string. The key/nonce lengths SHALL be validated at the boundary. Decryption with a wrong tag SHALL throw `CryptoError("CRYPTO.AEAD.DECRYPT_FAILED")` with no plaintext leakage. #### Scenario: AES-256-GCM round-trip - **WHEN** `aeadDecrypt("AES-256-GCM", key, aeadEncrypt("AES-256-GCM", key, p, { nonce, aad }), { nonce, aad })` is called - **THEN** the returned bytes equal `p` #### Scenario: ChaCha20-Poly1305 round-trip - **WHEN** the same round-trip is performed with `"ChaCha20-Poly1305"` - **THEN** the returned bytes equal the plaintext #### Scenario: Tampered ciphertext rejected - **WHEN** `aeadDecrypt` is called with a ciphertext whose tag was flipped - **THEN** it throws `CryptoError("CRYPTO.AEAD.DECRYPT_FAILED")` AND does not return any bytes #### Scenario: Wrong key length rejected - **WHEN** `aeadEncrypt("AES-256-GCM", key, p, { nonce })` is called with `key.length !== 32` - **THEN** it throws `CryptoError("CRYPTO.KEY.INVALID_LENGTH")` #### Scenario: Wrong nonce length rejected - **WHEN** AES-256-GCM nonce length is not 12 OR ChaCha20-Poly1305 nonce length is not 12 - **THEN** `aeadEncrypt` throws `CryptoError("CRYPTO.AEAD.INVALID_NONCE")` ### Requirement: Ed25519, X25519, ECDSA-P256 The package SHALL export namespaces `ed25519`, `x25519`, `ecdsaP256` with `generateKeypair`, `sign`/`agree`, and `verify` where applicable. #### Scenario: Ed25519 sign/verify round-trip - **WHEN** `ed25519.verify(pk, ed25519.sign(sk, m), m)` is called - **THEN** it returns `true` #### Scenario: Ed25519 mismatched message - **WHEN** `ed25519.verify(pk, signatureOfM, otherMessage)` is called - **THEN** it returns `false` #### Scenario: X25519 shared secret symmetry - **WHEN** both parties compute `x25519.agree(mySecret, theirPublic)` - **THEN** they arrive at the same shared secret #### Scenario: ECDSA-P256 sign/verify round-trip - **WHEN** `ecdsaP256.verify(pk, ecdsaP256.sign(sk, m), m)` is called - **THEN** it returns `true` ### Requirement: Deterministic RNG injection Every function that consumes randomness SHALL accept `{ rng? }` to override the default. The default randomness source lives in exactly one file (`src/random/random-bytes.ts`) and is the only place that reads `crypto.getRandomValues`. #### Scenario: Injected RNG used - **WHEN** `randomBytes(16, { rng: (n) => new Uint8Array(n).fill(7) })` is called - **THEN** it returns a 16-byte array of 0x07s #### Scenario: No module-scope non-determinism - **WHEN** `src/` is scanned for `crypto.getRandomValues`, `Math.random`, `Date.now` - **THEN** those symbols appear only inside `src/random/random-bytes.ts` ### Requirement: Structured errors never leak secrets All thrown errors SHALL be `CryptoError` subclasses with codes prefixed `CRYPTO.`. Error messages and `context` MUST NOT contain plaintext, key bytes, signatures, or authentication tags. Lengths, algorithm names, and boolean facts are allowed. #### Scenario: Decryption failure message is generic - **WHEN** `aeadDecrypt` fails - **THEN** the thrown `CryptoError`'s `context` contains at most `{ algorithm, ciphertextLength }` — never raw bytes ### Requirement: 100% test coverage `vitest.config.ts` MUST enforce `statements: 100`, `branches: 100`, `functions: 100`, `lines: 100`. Property-based tests (fast-check) cover: AEAD round-trip, Ed25519 sign/verify, X25519 symmetry, ECDSA-P256 round-trip, hash determinism, constant-time-equal invariants. #### Scenario: Coverage threshold enforced - **WHEN** `bunx turbo test:coverage --filter=@suaveplan/crypto` runs - **THEN** it exits non-zero if any metric drops below 100