6.3 KiB
Capability Spec Delta: crypto-cipher (crypto-completion)
Package: @suaveplan/crypto-cipher
Status: PROPOSED
Every requirement below carries a
**Verified by:**line at authoring time.
ADDED Requirements
Requirement: Streaming AEAD encryption / decryption
The package SHALL expose encryptStreamAead(source, options) and decryptStreamAead(source, options) async generators that consume a Uint8Array input stream chunk by chunk, attach a per-chunk authentication tag, and emit the encrypted (or decrypted) chunk stream. Each chunk SHALL use a per-chunk nonce derived deterministically from a base nonce plus a 32-bit chunk counter so the entire stream binds to a single key without nonce reuse.
Scenario: round-trip preserves bytes
- WHEN
encryptStreamAead(plaintext, { algorithm, key, baseNonce })is consumed and its output is fed todecryptStreamAead(ciphertext, { algorithm, key, baseNonce }) - THEN the concatenated output equals the original plaintext byte-for-byte
Scenario: tag mismatch detected on tampered chunk
- WHEN any byte of any encrypted chunk's tag is flipped before decryption
- THEN
decryptStreamAeadthrows aSuaveplanErrorwith codeCRYPTO_CIPHER.STREAM_TAG_MISMATCHandcontext.chunkIndexpoints at the tampered chunk
Scenario: chunk-counter overflow rejected
- WHEN the chunk counter reaches
2^32 - THEN the next chunk emission throws
CRYPTO_CIPHER.STREAM_OUT_OF_ORDERwithcontext.counter === 2 ** 32
Verified by: packages/crypto/crypto-cipher/src/streaming/streaming-aead/streaming-aead.test.ts::"round-trips a multi-chunk plaintext through encryptStreamAead and decryptStreamAead" AND packages/crypto/crypto-cipher/src/streaming/streaming-aead/streaming-aead.test.ts::"throws STREAM_TAG_MISMATCH when a chunk tag is flipped" AND packages/crypto/crypto-cipher/src/streaming/streaming-aead/streaming-aead.spec-conformance.test.ts::"throws STREAM_OUT_OF_ORDER on chunk-counter overflow"
Requirement: AbortSignal propagation through streaming AEAD
The package SHALL honour an optional AbortSignal passed via EncryptStreamAeadOptions.signal and DecryptStreamAeadOptions.signal. When the signal is aborted mid-stream the generator SHALL flush the in-flight chunk, release any pending cipher state, and throw SuaveplanError with code CRYPTO_CIPHER.STREAM_ABORTED.
Scenario: abort mid-stream rejects with STREAM_ABORTED
- WHEN an
AbortControlleris signalled whileencryptStreamAeadis mid-iteration - THEN the next
await generator.next()rejects withCRYPTO_CIPHER.STREAM_ABORTEDandcontext.chunksProcessedreflects the count emitted before abort
Verified by: packages/crypto/crypto-cipher/src/streaming/streaming-aead/streaming-aead.test.ts::"aborts mid-stream and rejects with STREAM_ABORTED"
Requirement: AES-GCM streaming engine adapter
The package SHALL expose aesGcmStreamingEngine: StreamingAeadEngine that wraps the existing aes-256-gcm primitive. Per-chunk nonce SHALL be a 12-byte big-endian concatenation of an 8-byte caller-supplied prefix and a 4-byte chunk counter (RFC 5116 §3.2 compatible). The engine SHALL reject baseNonce.length !== 8 with CRYPTO_CIPHER.STREAM_INVALID_NONCE_BASE.
Scenario: 8-byte base nonce accepted
- WHEN
aesGcmStreamingEngine.encrypt({ baseNonce: 8-byte prefix, key, plaintext, counter: 0 })is called - THEN the produced nonce equals
baseNonce ‖ counter_be32(0)(12 bytes total)
Scenario: wrong-length base nonce rejected
- WHEN
baseNonce.length === 12 - THEN the engine throws
CRYPTO_CIPHER.STREAM_INVALID_NONCE_BASEwithcontext.expectedLength === 8andcontext.actualLength === 12
Verified by: packages/crypto/crypto-cipher/src/streaming/streaming-aead-aes-gcm/streaming-aead-aes-gcm.test.ts::"derives 12-byte nonce from 8-byte base + counter" AND packages/crypto/crypto-cipher/src/streaming/streaming-aead-aes-gcm/streaming-aead-aes-gcm.spec-conformance.test.ts::"rejects wrong-length base nonce with STREAM_INVALID_NONCE_BASE"
Requirement: XChaCha20-Poly1305 streaming engine adapter
The package SHALL expose xchacha20StreamingEngine: StreamingAeadEngine that wraps the existing xchacha20-poly1305 primitive. Per-chunk nonce SHALL be a 24-byte big-endian concatenation of a 20-byte caller-supplied prefix and a 4-byte chunk counter. The engine SHALL reject baseNonce.length !== 20 with CRYPTO_CIPHER.STREAM_INVALID_NONCE_BASE.
Scenario: 20-byte base nonce accepted
- WHEN
xchacha20StreamingEngine.encrypt({ baseNonce: 20-byte prefix, key, plaintext, counter: 5 })is called - THEN the produced nonce equals
baseNonce ‖ counter_be32(5)(24 bytes total)
Scenario: wrong-length base nonce rejected
- WHEN
baseNonce.length === 24 - THEN the engine throws
CRYPTO_CIPHER.STREAM_INVALID_NONCE_BASEwithcontext.expectedLength === 20andcontext.actualLength === 24
Verified by: packages/crypto/crypto-cipher/src/streaming/streaming-aead-xchacha20/streaming-aead-xchacha20.test.ts::"derives 24-byte nonce from 20-byte base + counter" AND packages/crypto/crypto-cipher/src/streaming/streaming-aead-xchacha20/streaming-aead-xchacha20.spec-conformance.test.ts::"rejects wrong-length base nonce with STREAM_INVALID_NONCE_BASE"
Requirement: StreamingAeadEngine contract
The package SHALL define a StreamingAeadEngine interface in streaming-aead.types.ts with encrypt(input: StreamingChunkInput): Promise<StreamingChunkOutput> and decrypt(input: StreamingChunkInput): Promise<Uint8Array> methods, plus nonceBaseLength: number and tagLength: number constants. Both built-in engines (AES-GCM, XChaCha20-Poly1305) SHALL satisfy this interface and SHALL be substitutable for any caller-supplied engine.
Scenario: caller-supplied engine round-trips identically
- WHEN a custom
StreamingAeadEngineimplementation is passed toencryptStreamAeadand the same instance todecryptStreamAead - THEN the round-trip preserves bytes byte-for-byte (validated against an in-test spy engine that records every call)
Verified by: packages/crypto/crypto-cipher/src/streaming/streaming-aead/streaming-aead.spec-conformance.test.ts::"StreamingAeadEngine contract is stable for custom engines"