96 lines
6.3 KiB
Markdown
96 lines
6.3 KiB
Markdown
<!-- markdownlint-disable MD024 -->
|
|
|
|
# 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 to `decryptStreamAead(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** `decryptStreamAead` throws a `SuaveplanError` with code `CRYPTO_CIPHER.STREAM_TAG_MISMATCH` and `context.chunkIndex` points 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_ORDER` with `context.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 `AbortController` is signalled while `encryptStreamAead` is mid-iteration
|
|
- **THEN** the next `await generator.next()` rejects with `CRYPTO_CIPHER.STREAM_ABORTED` and `context.chunksProcessed` reflects 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_BASE` with `context.expectedLength === 8` and `context.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_BASE` with `context.expectedLength === 20` and `context.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 `StreamingAeadEngine` implementation is passed to `encryptStreamAead` and the same instance to `decryptStreamAead`
|
|
- **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"`
|
|
|
|
<!-- markdownlint-enable MD024 -->
|