first commit
This commit is contained in:
@@ -0,0 +1,75 @@
|
||||
# streaming-aead-aes-gcm
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
`streaming-aead-aes-gcm.ts` is the AES-256-GCM adapter that satisfies the `StreamingAeadEngine` contract used by `encryptStreamAead` / `decryptStreamAead`. It wraps the existing `aes-256-gcm` AEAD primitive with the per-chunk nonce-derivation and counter-discipline rules that the streaming layer requires. AES-256-GCM is the right choice when interoperability with FIPS-validated stacks or hardware AES-NI acceleration is required.
|
||||
|
||||
The engine declares an 8-byte caller-supplied base nonce; the per-chunk 12-byte nonce is `baseNonce ‖ counter_be32` (RFC 5116 §3.2 compatible). The high bit of the 32-bit counter carries the final-flag bit so the legitimate terminator chunk uses a different nonce than a truncated data chunk at the same counter index.
|
||||
|
||||
## Features
|
||||
|
||||
- Implements the `StreamingAeadEngine` contract — drop-in for the streaming generators.
|
||||
- 8-byte base nonce / 12-byte per-chunk nonce / 16-byte AEAD tag.
|
||||
- Counter-discipline guard: rejects negative or `> 2^32 - 1` counters with `STREAM_OUT_OF_ORDER`.
|
||||
- Base-nonce length guard: rejects any base nonce whose length is not exactly 8 bytes with `STREAM_INVALID_NONCE_BASE`.
|
||||
- Tag-mismatch promotion: open-side tag failures route to `STREAM_TAG_MISMATCH` (with the original `DECRYPT_FAILED` as cause) so streaming callers see the streaming-shaped error.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { aesGcmStreamingEngine } from "@suaveplan/crypto-cipher/streaming/streaming-aead-aes-gcm";
|
||||
import { encryptStreamAead, decryptStreamAead } from "@suaveplan/crypto-cipher/streaming/streaming-aead";
|
||||
|
||||
const key = crypto.getRandomValues(new Uint8Array(32));
|
||||
const baseNonce = crypto.getRandomValues(new Uint8Array(8));
|
||||
|
||||
for await (const chunk of encryptStreamAead(key, source(), {
|
||||
engine: aesGcmStreamingEngine,
|
||||
baseNonce,
|
||||
})) {
|
||||
sink.write(chunk);
|
||||
}
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { aesGcmStreamingEngine } from "@suaveplan/crypto-cipher/streaming/streaming-aead-aes-gcm";
|
||||
|
||||
// Direct seal/open use without the framing layer (e.g. embedding into a
|
||||
// custom transport). Always derive a fresh per-chunk nonce — never reuse a
|
||||
// (key, baseNonce, counter, finalFlag) tuple.
|
||||
const ct = await aesGcmStreamingEngine.seal({
|
||||
key,
|
||||
baseNonce,
|
||||
counter: 0,
|
||||
finalFlag: 0,
|
||||
data: plaintext,
|
||||
aad: contextBytes,
|
||||
});
|
||||
|
||||
const pt = await aesGcmStreamingEngine.open({
|
||||
key,
|
||||
baseNonce,
|
||||
counter: 0,
|
||||
finalFlag: 0,
|
||||
data: ct,
|
||||
aad: contextBytes,
|
||||
});
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `aesGcmStreamingEngine: StreamingAeadEngine` — frozen engine instance.
|
||||
- `BASE_NONCE_LENGTH: 8`
|
||||
- `NONCE_LENGTH: 12`
|
||||
- `TAG_LENGTH: 16`
|
||||
|
||||
Errors: `STREAM_INVALID_NONCE_BASE`, `STREAM_OUT_OF_ORDER`, `STREAM_TAG_MISMATCH`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
AES-GCM requires unique `(key, nonce)` pairs — reuse destroys both confidentiality and authenticity. The engine enforces this through deterministic nonce derivation: any caller that supplies the same base nonce and counter index will produce the same nonce, but the streaming generator increments the counter once per emitted chunk and binds the final-flag into the high bit of the counter so terminator and data chunks at the same index produce different nonces. Counter-overflow at `2^32` chunks short-circuits before reuse can occur; in practice this is well above the practical limit since each chunk is bounded by AES-GCM's `2^36 - 32`-byte plaintext limit per nonce.
|
||||
@@ -0,0 +1,66 @@
|
||||
# streaming-aead-xchacha20
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
`streaming-aead-xchacha20.ts` is the XChaCha20-Poly1305 adapter that satisfies the `StreamingAeadEngine` contract used by `encryptStreamAead` / `decryptStreamAead`. XChaCha20-Poly1305 is preferred over AES-GCM when the deployment runs without hardware AES acceleration or when the 24-byte extended nonce makes random-nonce schemes safer (the `2^96` random-nonce birthday-bound is comfortable, where AES-GCM's `2^48` is not).
|
||||
|
||||
The engine declares a 20-byte caller-supplied base nonce; the per-chunk 24-byte nonce is `baseNonce ‖ counter_be32`. The high bit of the 32-bit counter carries the final-flag so terminator and data chunks at the same counter index produce different nonces. This is the default streaming engine — callers who do not pass `engine:` to `encryptStreamAead` / `decryptStreamAead` get this one.
|
||||
|
||||
## Features
|
||||
|
||||
- Implements the `StreamingAeadEngine` contract — drop-in for the streaming generators.
|
||||
- 20-byte base nonce / 24-byte per-chunk nonce / 16-byte AEAD tag.
|
||||
- Counter-discipline guard: rejects negative or `> 2^32 - 1` counters with `STREAM_OUT_OF_ORDER`.
|
||||
- Base-nonce length guard: rejects any base nonce whose length is not exactly 20 bytes with `STREAM_INVALID_NONCE_BASE`.
|
||||
- Tag-mismatch promotion: open-side tag failures route to `STREAM_TAG_MISMATCH` (with the original `DECRYPT_FAILED` as cause) so streaming callers see the streaming-shaped error.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { xchacha20StreamingEngine } from "@suaveplan/crypto-cipher/streaming/streaming-aead-xchacha20";
|
||||
import { encryptStreamAead } from "@suaveplan/crypto-cipher/streaming/streaming-aead";
|
||||
|
||||
const key = crypto.getRandomValues(new Uint8Array(32));
|
||||
// xchacha20StreamingEngine is the default — passing it explicitly is optional.
|
||||
for await (const chunk of encryptStreamAead(key, source(), {
|
||||
engine: xchacha20StreamingEngine,
|
||||
})) {
|
||||
sink.write(chunk);
|
||||
}
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { xchacha20StreamingEngine } from "@suaveplan/crypto-cipher/streaming/streaming-aead-xchacha20";
|
||||
|
||||
// Use the engine directly without the framing layer to embed XChaCha20-Poly1305
|
||||
// into a custom transport. The 20-byte base nonce gives plenty of headroom for
|
||||
// random nonces if the caller derives one per-stream rather than per-chunk.
|
||||
const baseNonce = crypto.getRandomValues(new Uint8Array(20));
|
||||
|
||||
const ct = await xchacha20StreamingEngine.seal({
|
||||
key,
|
||||
baseNonce,
|
||||
counter: 0,
|
||||
finalFlag: 0,
|
||||
data: plaintext,
|
||||
aad: contextBytes,
|
||||
});
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `xchacha20StreamingEngine: StreamingAeadEngine` — frozen engine instance.
|
||||
- `BASE_NONCE_LENGTH: 20`
|
||||
- `NONCE_LENGTH: 24`
|
||||
- `TAG_LENGTH: 16`
|
||||
|
||||
Errors: `STREAM_INVALID_NONCE_BASE`, `STREAM_OUT_OF_ORDER`, `STREAM_TAG_MISMATCH`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
XChaCha20-Poly1305 (libsodium specification, RFC draft) extends the ChaCha20 nonce from 12 to 24 bytes via HChaCha20 sub-key derivation. The 24-byte nonce makes random nonces safe (`2^96` random nonces have a `~10^-19` collision probability), which is the basis for the streaming layer using a random base nonce plus a counter. Counter-overflow at `2^32` chunks short-circuits before reuse can occur. This adapter does not generate the base nonce — the streaming generator supplies it (random by default; deterministic via the `baseNonce` option for tests).
|
||||
@@ -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