7.6 KiB
Capability Spec Delta: crypto (crypto-completion)
Package: @suaveplan/crypto
Status: PROPOSED
Every requirement below carries a
**Verified by:**line at authoring time.
ADDED Requirements
Requirement: High-level streaming AEAD façade
The package SHALL expose encryptStreamAead(opts) and decryptStreamAead(opts) from aead/streaming/streaming-aead.ts that select the engine (xchacha20-poly1305 or aes-256-gcm) from opts.algorithm and delegate to the corresponding StreamingAeadEngine in @suaveplan/crypto-cipher. The package SHALL re-export the named engines aesGcmStreamingEngine and xchacha20StreamingEngine so callers wanting the engine directly do not need to depend on crypto-cipher.
Scenario: facade routes to AES-GCM engine
- WHEN
encryptStreamAead({ algorithm: "aes-256-gcm", key, baseNonce: 8-byte, plaintext })is called - THEN the produced ciphertext decrypts via the standalone
aesGcmStreamingEngineexported from this package, byte-for-byte
Scenario: facade routes to XChaCha20-Poly1305 engine
- WHEN
encryptStreamAead({ algorithm: "xchacha20-poly1305", key, baseNonce: 20-byte, plaintext })is called - THEN the produced ciphertext decrypts via the standalone
xchacha20StreamingEngineexported from this package, byte-for-byte
Scenario: unknown algorithm rejected
- WHEN
encryptStreamAead({ algorithm: "rot13" as never, ... })is called - THEN it throws
SuaveplanErrorwith codeCRYPTO.STREAM_AEAD_HEADER_INVALIDandcontext.algorithm === "rot13"
Verified by: packages/crypto/crypto/src/aead/streaming/streaming-aead.test.ts::"facade routes aes-256-gcm to aesGcmStreamingEngine" AND packages/crypto/crypto/src/aead/streaming/streaming-aead.test.ts::"facade routes xchacha20-poly1305 to xchacha20StreamingEngine" AND packages/crypto/crypto/src/aead/streaming/streaming-aead.spec-conformance.test.ts::"throws STREAM_AEAD_HEADER_INVALID on unknown algorithm"
Requirement: Web-Streams adapters for streaming AEAD
The package SHALL expose createEncryptionStream(opts) → TransformStream<Uint8Array, Uint8Array> and createDecryptionStream(opts) → TransformStream<Uint8Array, Uint8Array> from aead/streaming/web-streams.ts. These SHALL be drop-in pipeThrough targets for any Web-Streams ReadableStream<Uint8Array> source (e.g. Blob.stream(), Response.body, fetch().body). The package SHALL also expose collectStream(stream) → Promise<Uint8Array> (concatenates all chunks), createStreamKey(rng?) → Uint8Array (returns 32 random bytes), and createStreamEncryption(opts) (combined key + base-nonce + initialised stream factory).
Scenario: pipeThrough round-trips a Blob
- WHEN
blob.stream().pipeThrough(createEncryptionStream(opts)).pipeThrough(createDecryptionStream(opts))is collected viacollectStream - THEN the collected bytes equal the original blob bytes
Scenario: legacy aliases preserved
- WHEN
streamEncrypt,streamDecrypt,ChaCha20Stream,AESStreamare imported - THEN all four resolve to defined values that satisfy the documented upstream contract
Verified by: packages/crypto/crypto/src/aead/streaming/web-streams.test.ts::"pipeThrough round-trips a multi-chunk source through encrypt then decrypt" AND packages/crypto/crypto/src/aead/streaming/web-streams.test.ts::"streamEncrypt / streamDecrypt / ChaCha20Stream / AESStream aliases resolve and round-trip"
Requirement: Password-based encryption helpers
The package SHALL expose encryptWithPassword(password, plaintext, options?) and decryptWithPassword(password, ciphertext, options?) from password/encrypt-with-password.ts. The options.kdf field SHALL accept "argon2id" (default), "scrypt", or "pbkdf2". The output blob SHALL begin with a versioned 16-byte self-describing header { magic: "SVPP", version: 1, kdf, cipher, reserved, saltLen, nonceLen, kdfParamsLen } followed by salt, nonce, CBOR-encoded kdfParams, then AEAD ciphertext. decryptWithPassword SHALL parse the header to recover all parameters and SHALL NOT require any out-of-band metadata from the caller.
Scenario: round-trip with default Argon2id
- WHEN
encryptWithPassword("correct horse battery staple", plaintext)producesblobanddecryptWithPassword("correct horse battery staple", blob)is called - THEN the result equals
plaintextbyte-for-byte
Scenario: round-trip with scrypt KDF
- WHEN
encryptWithPassword(pw, plaintext, { kdf: "scrypt" })producesblobanddecryptWithPassword(pw, blob)is called - THEN the result equals
plaintextbyte-for-byte AND the blob's KDF byte signals scrypt
Scenario: blob header self-describes
- WHEN the same
(password, plaintext)is encrypted twice with differentkdfParams - THEN the two blobs have different KDF-params bytes in their headers AND both decrypt successfully without the caller passing
kdfParamstodecryptWithPassword
Scenario: unknown KDF rejected
- WHEN
encryptWithPassword(pw, plaintext, { kdf: "bcrypt" as never })is called - THEN it throws
SuaveplanErrorwith codeCRYPTO.PASSWORD_KDF_UNKNOWNandcontext.kdf === "bcrypt"
Scenario: corrupt header rejected
- WHEN
decryptWithPassword(pw, blob)is called with ablobwhose first four bytes are not"SVPP" - THEN it throws
SuaveplanErrorwith codeCRYPTO.PASSWORD_BLOB_HEADER_INVALID
Scenario: unsupported version rejected
- WHEN
decryptWithPassword(pw, blob)is called with ablobwhose version byte is99 - THEN it throws
SuaveplanErrorwith codeCRYPTO.PASSWORD_BLOB_VERSION_UNSUPPORTEDandcontext.version === 99
Verified by: packages/crypto/crypto/src/password/encrypt-with-password.test.ts::"round-trips with default Argon2id KDF" AND packages/crypto/crypto/src/password/encrypt-with-password.test.ts::"round-trips with scrypt KDF" AND packages/crypto/crypto/src/password/encrypt-with-password.test.ts::"blob header is self-describing across kdfParams variants" AND packages/crypto/crypto/src/password/encrypt-with-password.spec-conformance.test.ts::"throws PASSWORD_KDF_UNKNOWN on unknown KDF name" AND packages/crypto/crypto/src/password/encrypt-with-password.spec-conformance.test.ts::"throws PASSWORD_BLOB_HEADER_INVALID on corrupt magic bytes" AND packages/crypto/crypto/src/password/encrypt-with-password.spec-conformance.test.ts::"throws PASSWORD_BLOB_VERSION_UNSUPPORTED on unknown version byte"
Requirement: deriveKeyFromPassword
The package SHALL expose deriveKeyFromPassword(password, salt, options?) from password/derive-key-from-password.ts. The options SHALL accept kdf: "argon2id" | "scrypt" | "pbkdf2" (default "argon2id"), kdfParams: Record<string, number> (algorithm-specific), and dkLen: number (default 32). The function SHALL return a Uint8Array of length dkLen suitable for direct use as a symmetric AEAD key. Identical (password, salt, kdf, kdfParams) SHALL produce byte-identical output.
Scenario: deterministic given fixed inputs
- WHEN
deriveKeyFromPassword(pw, salt, { kdf: "scrypt", kdfParams: { N: 1024, r: 8, p: 1 } })is called twice - THEN both calls return byte-identical 32-byte Uint8Arrays
Scenario: dkLen honored
- WHEN
deriveKeyFromPassword(pw, salt, { kdf: "argon2id", dkLen: 64 })is called - THEN the result is a 64-byte Uint8Array
Verified by: packages/crypto/crypto/src/password/derive-key-from-password.test.ts::"deterministic for fixed password / salt / params" AND packages/crypto/crypto/src/password/derive-key-from-password.test.ts::"honors dkLen option"