first commit
This commit is contained in:
@@ -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.
|
||||
Reference in New Issue
Block a user