4.6 KiB
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<Uint8Array> 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<Uint8Array>or syncIterable<Uint8Array>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.
AbortSignalpropagation — aborting flushes the in-flight chunk and emitsSTREAM_ABORTED.- Optional
onHeader/onChunkcallbacks 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^32chunks — emitsSTREAM_OUT_OF_ORDER. - Tag-mismatch detection on every chunk emits
STREAM_TAG_MISMATCH. allowUnterminatedopt-in for streams produced by upstream tooling that omit the final-flag.
Usage: Basic Example
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
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<Uint8Array, void>— framessourcechunk-by-chunk and yields the wire bytes.decryptStreamAead(key, source, options?): AsyncGenerator<Uint8Array, void>— consumes wire bytes fromsource, 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.