first commit

This commit is contained in:
2026-05-29 08:20:56 +01:00
commit 949299f58d
6368 changed files with 527176 additions and 0 deletions

View File

@@ -0,0 +1,96 @@
# streaming-aead
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## 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 sync `Iterable<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.
- `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<Uint8Array, void>` — frames `source` chunk-by-chunk and yields the wire bytes.
- `decryptStreamAead(key, source, options?): AsyncGenerator<Uint8Array, void>` — 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.