# streaming-aead _(no submodules)_ ## Purpose `streaming-aead.ts` is the engine-agnostic streaming AEAD generator pair (`encryptStreamAead` / `decryptStreamAead`) in `@suaveplan/crypto-cipher`. It frames an arbitrary `AsyncIterable` source of plaintext into a self-describing wire format with per-chunk authentication and a final-flag terminator that binds the total chunk count into the AEAD tag, so silent truncation of the trailing chunk is impossible. The generators delegate per-chunk crypto to a pluggable `StreamingAeadEngine` (default: `xchacha20StreamingEngine`). The engine boundary is the substitution point that lets callers swap in AES-256-GCM, XChaCha20-Poly1305, or a hardware-backed engine without changing the framing protocol or the public API surface. ## Features - Async-generator API consumes any `AsyncIterable` or sync `Iterable` source. - Framed wire format: 8-byte header prefix + variable-length base nonce, then `[ciphertextLen BE | finalFlag | ciphertext‖tag]` per chunk. - Final-flag terminator chunk binds the total chunk count into the AEAD tag. - `AbortSignal` propagation — aborting flushes the in-flight chunk and emits `STREAM_ABORTED`. - Optional `onHeader` / `onChunk` callbacks for attestation and observability without breaking the streaming contract. - Engine pluggability (the default is XChaCha20-Poly1305; AES-256-GCM is available via `aesGcmStreamingEngine`). - Counter-overflow guard at `2^32` chunks — emits `STREAM_OUT_OF_ORDER`. - Tag-mismatch detection on every chunk emits `STREAM_TAG_MISMATCH`. - `allowUnterminated` opt-in for streams produced by upstream tooling that omit the final-flag. ## Usage: Basic Example ```ts import { encryptStreamAead, decryptStreamAead, } from "@suaveplan/crypto-cipher/streaming/streaming-aead"; async function* source() { yield new TextEncoder().encode("part one "); yield new TextEncoder().encode("part two"); } const key = crypto.getRandomValues(new Uint8Array(32)); const wire: Uint8Array[] = []; for await (const chunk of encryptStreamAead(key, source())) { wire.push(chunk); } async function* replay() { for (const c of wire) yield c; } const out: Uint8Array[] = []; for await (const chunk of decryptStreamAead(key, replay())) { out.push(chunk); } ``` ## Usage: Advanced Example ```ts import { encryptStreamAead, decryptStreamAead, } from "@suaveplan/crypto-cipher/streaming/streaming-aead"; import { aesGcmStreamingEngine } from "@suaveplan/crypto-cipher/streaming/streaming-aead-aes-gcm"; const key = crypto.getRandomValues(new Uint8Array(32)); const baseNonce = crypto.getRandomValues(new Uint8Array(8)); const ctrl = new AbortController(); setTimeout(() => ctrl.abort(), 250); // abort half-way through a long stream try { for await (const chunk of encryptStreamAead(key, longSource(), { engine: aesGcmStreamingEngine, baseNonce, signal: ctrl.signal, onHeader: (h) => console.log("header", h.engineId), onChunk: (t) => console.log("chunk", t.counter, t.finalFlag), })) { sink.write(chunk); } } catch (err) { if (err instanceof Error && (err as { code?: string }).code === "CRYPTO.CIPHER.STREAM_ABORTED") { console.warn("aborted at chunk", (err as { context?: { chunksProcessed: number } }).context?.chunksProcessed); } } ``` ## API Reference - `encryptStreamAead(key, source, options?): AsyncGenerator` — frames `source` chunk-by-chunk and yields the wire bytes. - `decryptStreamAead(key, source, options?): AsyncGenerator` — consumes wire bytes from `source`, yields recovered plaintext chunks. - `EncryptStreamAeadOptions { engine?, baseNonce?, signal?, onHeader?, onChunk? }`. - `DecryptStreamAeadOptions { engine?, allowUnterminated?, signal?, onHeader?, onChunk? }`. Errors: every failure path throws `CryptoCipherError` with one of `STREAM_TAG_MISMATCH`, `STREAM_OUT_OF_ORDER`, `STREAM_ABORTED`, `STREAM_INVALID_NONCE_BASE`. ## Implementation Notes The wire header is 8 bytes (magic[4] || engineWireId[1] || version[1] || baseNonceLen[2]) followed by the base nonce. Each chunk frame is `[ciphertextLen BE | finalFlag | ciphertext‖tag]`. The terminator frame uses an empty plaintext under finalFlag=1 so the AEAD tag binds the chunk count — a truncated stream that omits the terminator fails at decrypt time with `STREAM_TAG_MISMATCH`. The high bit of the 32-bit chunk counter carries the final-flag bit, so terminator and data chunks at the same counter index produce different nonces and cannot be confused.