Move docs/ package mirror into document-navigator plugin
Relocate the ~6k-file docs tree from the repo root into plugins/document-navigator/docs/ so the plugin is self-contained and portable. docs/superpowers/ (specs and plans) stays at the repo root. Moved via cp+rm rather than git mv because directory rename() fails with EPERM on this WSL /mnt DrvFs mount; git rename-detection pairs the identical-content files at commit time. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,8 @@
|
||||
# @suaveplan/crypto-cipher
|
||||
|
||||
## 0.0.0
|
||||
|
||||
- Initial scaffold under `crypto-primitive-replaceability` Phase 0.
|
||||
- AEAD primitives: aes-256-gcm, chacha20-poly1305, xchacha20-poly1305.
|
||||
- Noble-backed impls (`@noble/ciphers`); native stubs throw `PrimitiveNotImplemented`.
|
||||
- Nonces are caller-supplied; primitives validate length but never generate them.
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,79 @@
|
||||
# Crypto-Cipher Error Codes
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
Defines the stable error-code registry for `@suaveplan/crypto-cipher` using `defineCodes` from `@suaveplan/error`. Every validation error and decryption failure thrown by the cipher primitives carries one of these typed codes, giving callers a machine-readable signal that can be matched in switch statements or logged to structured observability pipelines without inspecting human-readable message strings.
|
||||
|
||||
The namespace `"CRYPTO.CIPHER"` is unique within the project, ensuring codes never collide with those in `@suaveplan/crypto-hash`, `@suaveplan/crypto-curve`, or other packages.
|
||||
|
||||
## Features
|
||||
|
||||
- `CRYPTO_CIPHER_CODES.INVALID_KEY_LENGTH` — thrown when a key of the wrong byte length is provided to any cipher primitive.
|
||||
- `CRYPTO_CIPHER_CODES.INVALID_NONCE_LENGTH` — thrown when a nonce of the wrong byte length is provided.
|
||||
- `CRYPTO_CIPHER_CODES.DECRYPT_FAILED` — thrown when AEAD authentication tag verification fails during decryption.
|
||||
- `CryptoCipherCode` union type — narrows codes accepted by `CryptoCipherError` constructors.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { CRYPTO_CIPHER_CODES } from "@suaveplan/crypto-cipher/errors";
|
||||
import { CryptoCipherError } from "@suaveplan/crypto-cipher/errors";
|
||||
|
||||
function handleCipherError(err: CryptoCipherError): void {
|
||||
switch (err.code) {
|
||||
case CRYPTO_CIPHER_CODES.INVALID_KEY_LENGTH:
|
||||
console.error("Wrong key length");
|
||||
break;
|
||||
case CRYPTO_CIPHER_CODES.DECRYPT_FAILED:
|
||||
console.error("Authentication tag mismatch");
|
||||
break;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { CRYPTO_CIPHER_CODES, CryptoCipherError } from "@suaveplan/crypto-cipher/errors";
|
||||
|
||||
function assertKeyLength(key: Uint8Array, expected: number, algorithm: string): void {
|
||||
if (key.length !== expected) {
|
||||
throw new CryptoCipherError(
|
||||
CRYPTO_CIPHER_CODES.INVALID_KEY_LENGTH,
|
||||
"error.crypto.cipher.invalid-key-length",
|
||||
{
|
||||
context: { algorithm, expected, actual: key.length },
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `CRYPTO_CIPHER_CODES`
|
||||
|
||||
```ts
|
||||
const CRYPTO_CIPHER_CODES: {
|
||||
readonly INVALID_KEY_LENGTH: "CRYPTO.CIPHER.INVALID_KEY_LENGTH";
|
||||
readonly INVALID_NONCE_LENGTH: "CRYPTO.CIPHER.INVALID_NONCE_LENGTH";
|
||||
readonly DECRYPT_FAILED: "CRYPTO.CIPHER.DECRYPT_FAILED";
|
||||
};
|
||||
```
|
||||
|
||||
Frozen registry produced by `defineCodes("CRYPTO.CIPHER", [...])`. Values are dot-separated canonical codes used in error serialisation, log queries, and alerting rules.
|
||||
|
||||
### `CryptoCipherCode`
|
||||
|
||||
```ts
|
||||
type CryptoCipherCode = (typeof CRYPTO_CIPHER_CODES)[keyof typeof CRYPTO_CIPHER_CODES];
|
||||
```
|
||||
|
||||
Union of all code string literals in `CRYPTO_CIPHER_CODES`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
`defineCodes` from `@suaveplan/error` produces a frozen object with string-literal values, giving both runtime and compile-time stability. Adding a new code requires extending the `as const` tuple and regenerating types; no numeric ID assignment or migration is required. The `DECRYPT_FAILED` code is intentionally generic — it does not reveal whether the failure was a tag mismatch, truncation, or corruption — to avoid leaking information that could assist an adaptive chosen-ciphertext attacker.
|
||||
@@ -0,0 +1,72 @@
|
||||
# CryptoCipherError
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
`CryptoCipherError` is the package-local error class for `@suaveplan/crypto-cipher`. It extends `SuaveplanError<CryptoCipherCode>` from `@suaveplan/error`, inheriting the full structured-error contract: a machine-readable typed code, an i18n message key, cause-chain traversal, Zod-validated wire format, and OTel span-event adaptation.
|
||||
|
||||
The class's only role is widening the protected `SuaveplanError` constructor to `public`, so cipher implementations can throw typed errors without needing internal access to the base class. This pattern is used consistently across all `@suaveplan/crypto-*` packages.
|
||||
|
||||
## Features
|
||||
|
||||
- Typed on `CryptoCipherCode` — the compiler enforces that only codes from `CRYPTO_CIPHER_CODES` are passed.
|
||||
- Accepts structured `context` via `SuaveplanErrorOptions` for attaching algorithm name, expected/actual lengths, and ciphertext sizes.
|
||||
- Full `cause` chain support for wrapping lower-level errors thrown by Noble or native cipher backends.
|
||||
- Serialisable to JSON wire format via `toJSON()`, compatible with cross-boundary error transport.
|
||||
- Adaptable to OTel span events via `toSpanEvent()` for distributed tracing.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { CryptoCipherError } from "@suaveplan/crypto-cipher/errors";
|
||||
import { CRYPTO_CIPHER_CODES } from "@suaveplan/crypto-cipher/errors";
|
||||
|
||||
throw new CryptoCipherError(
|
||||
CRYPTO_CIPHER_CODES.INVALID_NONCE_LENGTH,
|
||||
"error.crypto.cipher.invalid-nonce-length"
|
||||
);
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { CryptoCipherError } from "@suaveplan/crypto-cipher/errors";
|
||||
import { CRYPTO_CIPHER_CODES } from "@suaveplan/crypto-cipher/errors";
|
||||
|
||||
function wrapDecryptError(cause: unknown, algorithm: string, len: number): never {
|
||||
throw new CryptoCipherError(
|
||||
CRYPTO_CIPHER_CODES.DECRYPT_FAILED,
|
||||
"error.crypto.cipher.decrypt-failed",
|
||||
{
|
||||
cause,
|
||||
context: { algorithm, ciphertextLength: len },
|
||||
}
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `class CryptoCipherError extends SuaveplanError<CryptoCipherCode>`
|
||||
|
||||
#### `constructor(code: CryptoCipherCode, messageKey: string, options?: SuaveplanErrorOptions)`
|
||||
|
||||
| Parameter | Type | Description |
|
||||
|-----------|------|-------------|
|
||||
| `code` | `CryptoCipherCode` | One of the codes from `CRYPTO_CIPHER_CODES`. |
|
||||
| `messageKey` | `string` | Dotted i18n key for the message resolver hook. |
|
||||
| `options` | `SuaveplanErrorOptions` (optional) | `cause`, `context`, and other base-class options. |
|
||||
|
||||
Inherits from `SuaveplanError<CryptoCipherCode>`:
|
||||
|
||||
- `code: CryptoCipherCode`
|
||||
- `messageKey: string`
|
||||
- `context?: Record<string, unknown>`
|
||||
- `toJSON(): unknown`
|
||||
- `toSpanEvent(): OTelSpanEvent`
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The constructor body is empty and exists solely to change the access modifier from `protected` to `public`. Biome's `noUselessConstructor` rule is suppressed via an inline comment for this reason. The same pattern appears in `HashError` and the error classes of every other `@suaveplan/crypto-*` package, ensuring uniform error typing across the cryptography stack.
|
||||
@@ -0,0 +1,79 @@
|
||||
# aes-256-gcm
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
- [aes-256-gcm.select](./aes-256-gcm.select/aes-256-gcm.select.md) — <!-- begin:auto-docs --> _(no submodules)_- [aes-256-gcm.native](./impl/aes-256-gcm.native/aes-256-gcm.native.md) — <!-- begin:auto-docs --> _(no submodules)_ - [aes-256-gcm.native](./impl/aes-256-gcm.native/aes-256-gcm.native.md) — <!-- begin:auto-docs --> _(no submodules)_- [aes-256-gcm.noble](./impl/aes-256-gcm.noble/aes-256-gcm.noble.md) — <!-- begin:auto-docs --> _(no submodules)_ - [aes-256-gcm.noble](./impl/aes-256-gcm.noble/aes-256-gcm.noble.md) — <!-- begin:auto-docs --> _(no submodules)_- [aes-256-gcm](./aes-256-gcm.md) — <!-- begin:auto-docs --> _(no submodules)_- aes-256-gcm.select — <!-- begin:auto-docs --> _(no s…
|
||||
- [aes-256-gcm.select](./aes-256-gcm.select/aes-256-gcm.select.md) — <!-- begin:auto-docs --> _(no submodules)_- [aes-256-gcm.native](./impl/aes-256-gcm.native/aes-256-gcm.native.md) — <!-- begin:auto-docs --> _(no submodules)_- [aes-256-gcm.native](./impl/aes-256-gcm.native/aes-256-gcm.native.md) — <!-- begin:auto-docs --> _(no submodules)_- [aes-256-gcm.noble](./impl/aes-256-gcm.noble/aes-256-gcm.noble.md) — <!-- begin:auto-docs --> _(no submodules)_- [aes-256-gcm.noble](./impl/aes-256-gcm.noble/aes-256-gcm.noble.md) — <!-- begin:auto-docs --> _(no submodules)_- [aes-256-gcm](./aes-256-gcm.md) — <!-- begin:auto-docs --> _(no submodules)_- [aes-256-gcm.select](./aes-256-gcm.select/aes-256-gcm.select.md) — <!-- begin:auto-docs --> _(no submodules)_- [aes-256-gcm.native](./impl/aes-256-gcm.native/aes-256-gcm.native.md) — <!-- begin:auto-docs --> _(no submodules)_- [aes-256-gcm.native](./impl/aes-256-gcm.native/aes-256-gcm.native.md) — <!-- begin:auto-docs --> _(no submodules)_- [aes-256-gcm.noble](./impl/aes-256-gcm.noble/aes-256-gcm.noble.md) — <!-- begin:auto-docs --> _(no submodules)_- [aes-256-gcm.noble](./impl/aes-256-gcm.noble/aes-256-gcm.noble.md) — <!-- begin:auto-docs --> _(no submodules)_- [aes-256-gcm](./aes-256-gcm.md) — <!-- begin:auto-docs --> _(no submodules)_- aes-256-gcm.select — <!-- be…
|
||||
- [aes-256-gcm.select](./aes-256-gcm.select/aes-256-gcm.select.md) — <!-- begin:auto-docs --> _(no submodules)_- [aes-256-gcm.native](./impl/aes-256-gcm.native/aes-256-gcm.native.md) — <!-- begin:auto-docs --> _(no submodules)_- [aes-256-gcm.native](./impl/aes-256-gcm.native/aes-256-gcm.native.md) — <!-- begin:auto-docs --> _(no submodules)_- [aes-256-gcm.noble](./impl/aes-256-gcm.noble/aes-256-gcm.noble.md) — <!-- begin:auto-docs --> _(no submodules)_- [aes-256-gcm.noble](./impl/aes-256-gcm.noble/aes-256-gcm.noble.md) — <!-- begin:auto-docs --> _(no submodules)_- [aes-256-gcm](./aes-256-gcm.md) — <!-- begin:auto-docs --> _(no submodules)_- aes-256-gcm.select — <!-- begin:auto-docs --> _(no…
|
||||
- [aes-256-gcm.select](./aes-256-gcm.select/aes-256-gcm.select.md) — <!-- begin:auto-docs --> _(no submodules)_- [aes-256-gcm.native](./impl/aes-256-gcm.native/aes-256-gcm.native.md) — <!-- begin:auto-docs --> _(no submodules)_ - [aes-256-gcm.native](./impl/aes-256-gcm.native/aes-256-gcm.native.md) — <!-- begin:auto-docs --> _(no submodules)_- [aes-256-gcm.noble](./impl/aes-256-gcm.noble/aes-256-gcm.noble.md) — <!-- begin:auto-docs --> _(no submodules)_ - [aes-256-gcm.noble](./impl/aes-256-gcm.noble/aes-256-gcm.noble.md) — <!-- begin:auto-docs --> _(no submodules)_- [aes-256-gcm](./aes-256-gcm.md) — <!-- begin:auto-docs --> _(no submodules)_
|
||||
## Purpose
|
||||
|
||||
`aes-256-gcm.ts` is the stable public entry point for the AES-256-GCM AEAD primitive in `@suaveplan/crypto-cipher`. It re-exports everything from the generated `aes-256-gcm.select.ts`, which forwards to either the Noble-backed or native implementation according to `crypto.config.ts`.
|
||||
|
||||
AES-256-GCM (NIST SP 800-38D) is a widely deployed authenticated encryption scheme that provides both confidentiality and integrity. It is mandatory in TLS 1.3, common in cloud storage encryption, and natively available in WebCrypto. This entry point provides the stable import path and exposes the constants `KEY_LENGTH`, `NONCE_LENGTH`, and `TAG_LENGTH`.
|
||||
|
||||
## Features
|
||||
|
||||
- `encrypt(key, nonce, plaintext, aad?)` — AEAD encryption; returns `ciphertext ‖ tag`.
|
||||
- `decrypt(key, nonce, ciphertext, aad?)` — AEAD decryption; throws `CryptoCipherError` on tag mismatch.
|
||||
- `KEY_LENGTH = 32` — 256-bit key requirement.
|
||||
- `NONCE_LENGTH = 12` — 96-bit (12-byte) nonce as per NIST recommendation.
|
||||
- `TAG_LENGTH = 16` — 128-bit authentication tag.
|
||||
- Key and nonce length validated before any cipher operation.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { encrypt, decrypt, KEY_LENGTH, NONCE_LENGTH } from "@suaveplan/crypto-cipher/aes-256-gcm";
|
||||
|
||||
const key = crypto.getRandomValues(new Uint8Array(KEY_LENGTH));
|
||||
const nonce = crypto.getRandomValues(new Uint8Array(NONCE_LENGTH));
|
||||
const plaintext = new TextEncoder().encode("secret");
|
||||
|
||||
const ciphertext = encrypt(key, nonce, plaintext);
|
||||
const recovered = decrypt(key, nonce, ciphertext);
|
||||
console.log(new TextDecoder().decode(recovered)); // "secret"
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { encrypt, decrypt, KEY_LENGTH, NONCE_LENGTH } from "@suaveplan/crypto-cipher/aes-256-gcm";
|
||||
|
||||
// Encrypt with Additional Authenticated Data (AAD) for binding.
|
||||
function encryptWithContext(
|
||||
key: Uint8Array,
|
||||
plaintext: Uint8Array,
|
||||
context: string
|
||||
): { nonce: Uint8Array; ciphertext: Uint8Array } {
|
||||
const nonce = crypto.getRandomValues(new Uint8Array(NONCE_LENGTH));
|
||||
const aad = new TextEncoder().encode(context);
|
||||
const ciphertext = encrypt(key, nonce, plaintext, aad);
|
||||
return { nonce, ciphertext };
|
||||
}
|
||||
|
||||
function decryptWithContext(
|
||||
key: Uint8Array,
|
||||
nonce: Uint8Array,
|
||||
ciphertext: Uint8Array,
|
||||
context: string
|
||||
): Uint8Array {
|
||||
const aad = new TextEncoder().encode(context);
|
||||
return decrypt(key, nonce, ciphertext, aad); // throws CryptoCipherError on mismatch
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
All symbols re-exported from `aes-256-gcm.select.ts`:
|
||||
|
||||
- `encrypt(key, nonce, plaintext, aad?): Uint8Array`
|
||||
- `decrypt(key, nonce, ciphertext, aad?): Uint8Array`
|
||||
- `KEY_LENGTH: 32`
|
||||
- `NONCE_LENGTH: 12`
|
||||
- `TAG_LENGTH: 16`
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
Nonce generation is the caller's responsibility — this module never generates nonces internally. For AES-256-GCM, nonces must never be reused with the same key; a reused (key, nonce) pair destroys both confidentiality and authenticity. Callers should use random 12-byte nonces (96-bit) or a counter-based scheme. The 16-byte authentication tag is appended to the ciphertext in the output of `encrypt` and expected at the end of the ciphertext in `decrypt`.
|
||||
@@ -0,0 +1,62 @@
|
||||
# aes-256-gcm.select
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
`aes-256-gcm.select.ts` is the build-time primitive selector for AES-256-GCM in `@suaveplan/crypto-cipher`. Generated by `bun scripts/gen-crypto-selects.ts`, it re-exports one backend — either `aes-256-gcm.noble.ts` or `aes-256-gcm.native.ts` — based on the `aes-256-gcm` field in `crypto.config.ts`.
|
||||
|
||||
Callers import through the stable `aes-256-gcm.ts` entry point. The selector indirection allows backend changes without touching any caller import paths. Currently the Noble backend is selected because the native stub throws `PrimitiveNotImplemented`.
|
||||
|
||||
## Features
|
||||
|
||||
- Re-exports `encrypt`, `decrypt`, `KEY_LENGTH`, `NONCE_LENGTH`, `TAG_LENGTH`.
|
||||
- Generated — never hand-edited.
|
||||
- Controlled by `crypto.config.ts`: set `aes-256-gcm` to `'noble'` or `'native'`.
|
||||
- Enables dead-code elimination of the inactive backend.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { encrypt, KEY_LENGTH, NONCE_LENGTH } from "@suaveplan/crypto-cipher/aes-256-gcm";
|
||||
|
||||
const key = crypto.getRandomValues(new Uint8Array(KEY_LENGTH));
|
||||
const nonce = crypto.getRandomValues(new Uint8Array(NONCE_LENGTH));
|
||||
const ct = encrypt(key, nonce, new TextEncoder().encode("hello"));
|
||||
console.log(ct.length); // 16 + 5 = 21 (ciphertext + tag)
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { decrypt, NONCE_LENGTH } from "@suaveplan/crypto-cipher/aes-256-gcm";
|
||||
import { CryptoCipherError } from "@suaveplan/crypto-cipher/errors";
|
||||
|
||||
function safeDecrypt(key: Uint8Array, nonce: Uint8Array, ct: Uint8Array): Uint8Array | null {
|
||||
try {
|
||||
return decrypt(key, nonce, ct);
|
||||
} catch (err) {
|
||||
if (err instanceof CryptoCipherError) return null;
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
Re-exports from the active implementation:
|
||||
|
||||
- `encrypt(key: Uint8Array, nonce: Uint8Array, plaintext: Uint8Array, aad?: Uint8Array): Uint8Array`
|
||||
- `decrypt(key: Uint8Array, nonce: Uint8Array, ciphertext: Uint8Array, aad?: Uint8Array): Uint8Array`
|
||||
- `KEY_LENGTH: 32`
|
||||
- `NONCE_LENGTH: 12`
|
||||
- `TAG_LENGTH: 16`
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
This file is a generated one-liner and is marked `/* GENERATED — DO NOT EDIT */`. The generator resolves the `aes-256-gcm` backend from `crypto.config.ts` and emits the corresponding `export *` line. Running the generator is idempotent. Do not add logic here.
|
||||
|
||||
### Selection Logic
|
||||
|
||||
At build time `crypto.config.ts` chooses between the Noble `@noble/ciphers/aes` implementation and the native-engine stub for AES-256-GCM; the selector exported here reads the generated `crypto-selects` map and dispatches every `encrypt` / `decrypt` call to the chosen impl. Nonce is caller-supplied per the package-wide rule — this module does not generate nonces internally.
|
||||
@@ -0,0 +1,69 @@
|
||||
# aes-256-gcm.native
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
`aes-256-gcm.native.ts` is the native-backend placeholder for AES-256-GCM in `@suaveplan/crypto-cipher`. It exports `encrypt`, `decrypt`, `KEY_LENGTH`, `NONCE_LENGTH`, and `TAG_LENGTH` with the same types as the Noble implementation, but `encrypt` and `decrypt` throw `PrimitiveNotImplemented("aes-256-gcm")` unconditionally.
|
||||
|
||||
AES-256-GCM is natively available via `SubtleCrypto.encrypt` with `{ name: "AES-GCM" }` in all modern browsers and in Node.js via the `node:crypto` module. A future native implementation would delegate to these platform APIs, potentially enabling hardware-accelerated encryption on supported hardware. This stub keeps the selector layout complete until that work is ready.
|
||||
|
||||
## Features
|
||||
|
||||
- Exports `encrypt`, `decrypt`, `KEY_LENGTH`, `NONCE_LENGTH`, `TAG_LENGTH` — same surface as the Noble file.
|
||||
- `encrypt` and `decrypt` throw `PrimitiveNotImplemented("aes-256-gcm")`.
|
||||
- `KEY_LENGTH = 32`, `NONCE_LENGTH = 12`, `TAG_LENGTH = 16` constants are usable even when the algorithm is not implemented.
|
||||
- Zero runtime dependencies beyond `@suaveplan/crypto-utils/errors`.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
// Active only when crypto.config.ts sets aes-256-gcm to 'native'.
|
||||
import { encrypt, KEY_LENGTH, NONCE_LENGTH } from "@suaveplan/crypto-cipher/aes-256-gcm";
|
||||
|
||||
try {
|
||||
encrypt(
|
||||
new Uint8Array(KEY_LENGTH),
|
||||
new Uint8Array(NONCE_LENGTH),
|
||||
new Uint8Array(16)
|
||||
);
|
||||
} catch (err) {
|
||||
// PrimitiveNotImplemented: aes-256-gcm
|
||||
console.error(err);
|
||||
}
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
// The constants are still safe to use regardless of the selected backend.
|
||||
import { KEY_LENGTH, NONCE_LENGTH } from "@suaveplan/crypto-cipher/aes-256-gcm";
|
||||
|
||||
function allocateCipherBuffers(): { key: Uint8Array; nonce: Uint8Array } {
|
||||
return {
|
||||
key: crypto.getRandomValues(new Uint8Array(KEY_LENGTH)),
|
||||
nonce: crypto.getRandomValues(new Uint8Array(NONCE_LENGTH)),
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `encrypt(_key, _nonce, _plaintext, _aad?): Uint8Array`
|
||||
|
||||
Throws `PrimitiveNotImplemented("aes-256-gcm")`.
|
||||
|
||||
### `decrypt(_key, _nonce, _ciphertext, _aad?): Uint8Array`
|
||||
|
||||
Throws `PrimitiveNotImplemented("aes-256-gcm")`.
|
||||
|
||||
### Constants
|
||||
|
||||
- `KEY_LENGTH = 32`
|
||||
- `NONCE_LENGTH = 12`
|
||||
- `TAG_LENGTH = 16`
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
Exporting constants even in the placeholder makes the file drop-in compatible with the Noble implementation for callers that only import constants (e.g. to allocate buffers). When the native implementation is ready and passes NIST SP 800-38D CAVP test vectors, the placeholder functions are replaced and the selector regenerated. The implementation will need to handle the synchronous/asynchronous impedance mismatch since `SubtleCrypto` is async and this interface is synchronous — likely via Node.js `crypto.createCipheriv` for server-side use.
|
||||
@@ -0,0 +1,84 @@
|
||||
# aes-256-gcm.noble
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
`aes-256-gcm.noble.ts` is the Noble-backed concrete implementation of AES-256-GCM in `@suaveplan/crypto-cipher`. It uses `gcm` from `@noble/ciphers/aes`, validates key and nonce lengths before every operation, and wraps decryption failures in a typed `CryptoCipherError`.
|
||||
|
||||
AES-256-GCM (NIST SP 800-38D) provides authenticated encryption with associated data (AEAD). The authentication tag is appended to the ciphertext by Noble's `encrypt` and verified before returning plaintext from `decrypt`. This file is the production implementation; `aes-256-gcm.native.ts` is the placeholder.
|
||||
|
||||
## Features
|
||||
|
||||
- `encrypt(key, nonce, plaintext, aad?)` — validates lengths then delegates to Noble `gcm`.
|
||||
- `decrypt(key, nonce, ciphertext, aad?)` — validates lengths, delegates to Noble, wraps auth failures in `CryptoCipherError(DECRYPT_FAILED)`.
|
||||
- Exports `KEY_LENGTH = 32`, `NONCE_LENGTH = 12`, `TAG_LENGTH = 16`.
|
||||
- Validation throws `CryptoCipherError(INVALID_KEY_LENGTH)` or `INVALID_NONCE_LENGTH` with structured context.
|
||||
- Noble appends/strips the 16-byte GCM tag automatically.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { encrypt, decrypt, KEY_LENGTH, NONCE_LENGTH } from "@suaveplan/crypto-cipher/aes-256-gcm";
|
||||
|
||||
const key = crypto.getRandomValues(new Uint8Array(KEY_LENGTH));
|
||||
const nonce = crypto.getRandomValues(new Uint8Array(NONCE_LENGTH));
|
||||
const plaintext = new TextEncoder().encode("hello AES-GCM");
|
||||
|
||||
const ciphertext = encrypt(key, nonce, plaintext);
|
||||
const recovered = decrypt(key, nonce, ciphertext);
|
||||
console.log(new TextDecoder().decode(recovered)); // "hello AES-GCM"
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { encrypt, decrypt, KEY_LENGTH, NONCE_LENGTH } from "@suaveplan/crypto-cipher/aes-256-gcm";
|
||||
|
||||
// Encrypt a record with a session ID bound as AAD.
|
||||
function encryptRecord(
|
||||
key: Uint8Array,
|
||||
sessionId: string,
|
||||
record: Uint8Array
|
||||
): { nonce: Uint8Array; ciphertext: Uint8Array } {
|
||||
const nonce = crypto.getRandomValues(new Uint8Array(NONCE_LENGTH));
|
||||
const aad = new TextEncoder().encode(sessionId);
|
||||
return { nonce, ciphertext: encrypt(key, nonce, record, aad) };
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `encrypt(key: Uint8Array, nonce: Uint8Array, plaintext: Uint8Array, aad?: Uint8Array): Uint8Array`
|
||||
|
||||
| Parameter | Type | Description |
|
||||
|-----------|------|-------------|
|
||||
| `key` | `Uint8Array` | 32-byte AES-256 key. |
|
||||
| `nonce` | `Uint8Array` | 12-byte GCM nonce. Must not be reused with the same key. |
|
||||
| `plaintext` | `Uint8Array` | Plaintext bytes to encrypt. |
|
||||
| `aad` | `Uint8Array` (optional) | Additional authenticated data (not encrypted, but authenticated). |
|
||||
|
||||
**Returns:** `ciphertext ‖ tag` — length is `plaintext.length + 16`.
|
||||
|
||||
**Throws:** `CryptoCipherError(INVALID_KEY_LENGTH)` or `CryptoCipherError(INVALID_NONCE_LENGTH)`.
|
||||
|
||||
### `decrypt(key: Uint8Array, nonce: Uint8Array, ciphertext: Uint8Array, aad?: Uint8Array): Uint8Array`
|
||||
|
||||
| Parameter | Type | Description |
|
||||
|-----------|------|-------------|
|
||||
| `ciphertext` | `Uint8Array` | `ciphertext ‖ tag` produced by `encrypt`. |
|
||||
|
||||
**Returns:** Plaintext.
|
||||
|
||||
**Throws:** `CryptoCipherError(DECRYPT_FAILED)` if the authentication tag does not match.
|
||||
|
||||
### Constants
|
||||
|
||||
- `KEY_LENGTH = 32`
|
||||
- `NONCE_LENGTH = 12`
|
||||
- `TAG_LENGTH = 16`
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
Noble's `gcm(key, nonce, aad)` returns a cipher object with `.encrypt(plaintext)` and `.decrypt(ciphertext)` methods. The AAD parameter is passed at construction time. Length validation runs before the cipher is constructed, so wrong-length keys or nonces surface as `CryptoCipherError` rather than cryptic Noble-internal errors. Noble throws when the authentication tag mismatches during decryption; the catch block converts this to `CryptoCipherError(DECRYPT_FAILED)` with the algorithm name and ciphertext length as context.
|
||||
@@ -0,0 +1,67 @@
|
||||
# chacha20-poly1305
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
- [chacha20-poly1305.select](./chacha20-poly1305.select/chacha20-poly1305.select.md) — <!-- begin:auto-docs --> _(no submodules)_- [chacha20-poly1305.native](./impl/chacha20-poly1305.native/chacha20-poly1305.native.md) — <!-- begin:auto-docs --> _(no submodules)_ - [chacha20-poly1305.native](./impl/chacha20-poly1305.native/chacha20-poly1305.native.md) — <!-- begin:auto-docs --> _(no submodules)_- [chacha20-poly1305.noble](./impl/chacha20-poly1305.noble/chacha20-poly1305.noble.md) — <!-- begin:auto-docs --> _(no submodules)_ - [chacha20-poly1305.noble](./impl/chacha20-poly1305.noble/chacha20-poly1305.noble.md) — <!-- begin:auto-docs --> _(no submodules)_- [chacha20-poly1305](./chacha20-poly1305.md) — <!-- begin:auto-docs --> _(no submodules)_- chacha20-poly1305.select — <!-- begin:auto-docs -->…
|
||||
- [chacha20-poly1305.select](./chacha20-poly1305.select/chacha20-poly1305.select.md) — <!-- begin:auto-docs --> _(no submodules)_- [chacha20-poly1305.native](./impl/chacha20-poly1305.native/chacha20-poly1305.native.md) — <!-- begin:auto-docs --> _(no submodules)_- [chacha20-poly1305.native](./impl/chacha20-poly1305.native/chacha20-poly1305.native.md) — <!-- begin:auto-docs --> _(no submodules)_- [chacha20-poly1305.noble](./impl/chacha20-poly1305.noble/chacha20-poly1305.noble.md) — <!-- begin:auto-docs --> _(no submodules)_- [chacha20-poly1305.noble](./impl/chacha20-poly1305.noble/chacha20-poly1305.noble.md) — <!-- begin:auto-docs --> _(no submodules)_- [chacha20-poly1305](./chacha20-poly1305.md) — <!-- begin:auto-docs --> _(no submodules)_- [chacha20-poly1305.select](./chacha20-poly1305.select/chacha20-poly1305.select.md) — <!-- begin:auto-docs --> _(no submodules)_- [chacha20-poly1305.native](./impl/chacha20-poly1305.native/chacha20-poly1305.native.md) — <!-- begin:auto-docs --> _(no submodules)_- [chacha20-poly1305.native](./impl/chacha20-poly1305.native/chacha20-poly1305.native.md) — <!-- begin:auto-docs --> _(no submodules)_- [chacha20-poly1305.noble](./impl/chacha20-poly1305.noble/chacha20-poly1305.noble.md) — <!-- begin:auto-docs --> _(no submodules)_- [chacha20-poly1305.noble](./impl/chacha20-poly1305.noble/chacha20-poly1305.noble.md) — <!-- begin:auto-docs --> _(no submodules)_- [chacha20-poly1305](./chacha20-poly1305.md) — <!-- begin:auto-docs --> _(no submodules)_- chacha20-poly1305.select — <…
|
||||
- [chacha20-poly1305.select](./chacha20-poly1305.select/chacha20-poly1305.select.md) — <!-- begin:auto-docs --> _(no submodules)_- [chacha20-poly1305.native](./impl/chacha20-poly1305.native/chacha20-poly1305.native.md) — <!-- begin:auto-docs --> _(no submodules)_- [chacha20-poly1305.native](./impl/chacha20-poly1305.native/chacha20-poly1305.native.md) — <!-- begin:auto-docs --> _(no submodules)_- [chacha20-poly1305.noble](./impl/chacha20-poly1305.noble/chacha20-poly1305.noble.md) — <!-- begin:auto-docs --> _(no submodules)_- [chacha20-poly1305.noble](./impl/chacha20-poly1305.noble/chacha20-poly1305.noble.md) — <!-- begin:auto-docs --> _(no submodules)_- [chacha20-poly1305](./chacha20-poly1305.md) — <!-- begin:auto-docs --> _(no submodules)_- chacha20-poly1305.select — <!-- begin:auto-docs -->…
|
||||
- [chacha20-poly1305.select](./chacha20-poly1305.select/chacha20-poly1305.select.md) — <!-- begin:auto-docs --> _(no submodules)_- [chacha20-poly1305.native](./impl/chacha20-poly1305.native/chacha20-poly1305.native.md) — <!-- begin:auto-docs --> _(no submodules)_ - [chacha20-poly1305.native](./impl/chacha20-poly1305.native/chacha20-poly1305.native.md) — <!-- begin:auto-docs --> _(no submodules)_- [chacha20-poly1305.noble](./impl/chacha20-poly1305.noble/chacha20-poly1305.noble.md) — <!-- begin:auto-docs --> _(no submodules)_ - [chacha20-poly1305.noble](./impl/chacha20-poly1305.noble/chacha20-poly1305.noble.md) — <!-- begin:auto-docs --> _(no submodules)_- [chacha20-poly1305](./chacha20-poly1305.md) — <!-- begin:auto-docs --> _(no submodules)_
|
||||
## Purpose
|
||||
|
||||
`chacha20-poly1305.ts` is the stable public entry point for the ChaCha20-Poly1305 AEAD primitive in `@suaveplan/crypto-cipher`. It re-exports everything from the generated `chacha20-poly1305.select.ts`, which forwards to either the Noble-backed or native implementation based on `crypto.config.ts`.
|
||||
|
||||
ChaCha20-Poly1305 (RFC 8439) is a stream-cipher-based AEAD algorithm that provides authenticated encryption without the hardware AES acceleration requirement. It is preferred over AES-GCM on platforms that lack AES-NI instructions (mobile CPUs, edge nodes, IoT devices). TLS 1.3 includes it as a mandatory cipher suite alongside AES-128-GCM and AES-256-GCM.
|
||||
|
||||
## Features
|
||||
|
||||
- `encrypt(key, nonce, plaintext, aad?)` — AEAD encryption; returns `ciphertext ‖ tag`.
|
||||
- `decrypt(key, nonce, ciphertext, aad?)` — AEAD decryption; throws `CryptoCipherError` on failure.
|
||||
- `KEY_LENGTH = 32` — 256-bit key.
|
||||
- `NONCE_LENGTH = 12` — 96-bit nonce.
|
||||
- `TAG_LENGTH = 16` — 128-bit Poly1305 authentication tag.
|
||||
- Key and nonce length validated before every operation.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { encrypt, decrypt, KEY_LENGTH, NONCE_LENGTH } from "@suaveplan/crypto-cipher/chacha20-poly1305";
|
||||
|
||||
const key = crypto.getRandomValues(new Uint8Array(KEY_LENGTH));
|
||||
const nonce = crypto.getRandomValues(new Uint8Array(NONCE_LENGTH));
|
||||
const plaintext = new TextEncoder().encode("chacha test");
|
||||
|
||||
const ciphertext = encrypt(key, nonce, plaintext);
|
||||
const recovered = decrypt(key, nonce, ciphertext);
|
||||
console.log(new TextDecoder().decode(recovered)); // "chacha test"
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { encrypt, decrypt, KEY_LENGTH, NONCE_LENGTH } from "@suaveplan/crypto-cipher/chacha20-poly1305";
|
||||
|
||||
// Seal a message with a request ID as AAD.
|
||||
function sealMessage(key: Uint8Array, requestId: string, body: Uint8Array): {
|
||||
nonce: Uint8Array;
|
||||
sealed: Uint8Array;
|
||||
} {
|
||||
const nonce = crypto.getRandomValues(new Uint8Array(NONCE_LENGTH));
|
||||
const aad = new TextEncoder().encode(requestId);
|
||||
return { nonce, sealed: encrypt(key, nonce, body, aad) };
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
All symbols re-exported from `chacha20-poly1305.select.ts`:
|
||||
|
||||
- `encrypt(key, nonce, plaintext, aad?): Uint8Array`
|
||||
- `decrypt(key, nonce, ciphertext, aad?): Uint8Array`
|
||||
- `KEY_LENGTH: 32`
|
||||
- `NONCE_LENGTH: 12`
|
||||
- `TAG_LENGTH: 16`
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
This file is a zero-logic re-export barrel providing the stable import path `@suaveplan/crypto-cipher/chacha20-poly1305`. ChaCha20-Poly1305 uses a 12-byte nonce, which is safe to use with random generation for most use cases — at 2⁹⁶ possible nonces, the birthday bound for collision is comfortably above typical message volumes. For higher message counts, consider XChaCha20-Poly1305, which uses a 24-byte nonce. Nonces must never be reused with the same key.
|
||||
@@ -0,0 +1,59 @@
|
||||
# chacha20-poly1305.select
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
`chacha20-poly1305.select.ts` is the build-time primitive selector for ChaCha20-Poly1305 in `@suaveplan/crypto-cipher`. Generated by `bun scripts/gen-crypto-selects.ts`, it re-exports one backend — `chacha20-poly1305.noble.ts` or `chacha20-poly1305.native.ts` — based on the `chacha20-poly1305` entry in `crypto.config.ts`.
|
||||
|
||||
Callers import through `chacha20-poly1305.ts` and never reference this file directly. The selector decouples the public API from the concrete implementation, enabling backend switches with no changes to caller code.
|
||||
|
||||
## Features
|
||||
|
||||
- Re-exports `encrypt`, `decrypt`, `KEY_LENGTH`, `NONCE_LENGTH`, `TAG_LENGTH`.
|
||||
- Generated — never hand-edited.
|
||||
- Controlled by `crypto.config.ts`: set `chacha20-poly1305` to `'noble'` or `'native'`.
|
||||
- Enables dead-code elimination of the inactive backend at bundle time.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { encrypt, KEY_LENGTH, NONCE_LENGTH } from "@suaveplan/crypto-cipher/chacha20-poly1305";
|
||||
|
||||
const key = crypto.getRandomValues(new Uint8Array(KEY_LENGTH));
|
||||
const nonce = crypto.getRandomValues(new Uint8Array(NONCE_LENGTH));
|
||||
const ct = encrypt(key, nonce, new TextEncoder().encode("test"));
|
||||
console.log(ct.length); // 4 + 16 = 20
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { decrypt } from "@suaveplan/crypto-cipher/chacha20-poly1305";
|
||||
import { CryptoCipherError } from "@suaveplan/crypto-cipher/errors";
|
||||
import { CRYPTO_CIPHER_CODES } from "@suaveplan/crypto-cipher/errors";
|
||||
|
||||
function isDecryptError(err: unknown): err is CryptoCipherError {
|
||||
return err instanceof CryptoCipherError &&
|
||||
err.code === CRYPTO_CIPHER_CODES.DECRYPT_FAILED;
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
Re-exports from the active implementation:
|
||||
|
||||
- `encrypt(key: Uint8Array, nonce: Uint8Array, plaintext: Uint8Array, aad?: Uint8Array): Uint8Array`
|
||||
- `decrypt(key: Uint8Array, nonce: Uint8Array, ciphertext: Uint8Array, aad?: Uint8Array): Uint8Array`
|
||||
- `KEY_LENGTH: 32`
|
||||
- `NONCE_LENGTH: 12`
|
||||
- `TAG_LENGTH: 16`
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
This generated file contains `export * from "./impl/chacha20-poly1305.noble.js"` and is marked `/* GENERATED — DO NOT EDIT */`. The generator is idempotent and safe to run at any time. Do not add logic here — it will be overwritten on the next generator run.
|
||||
|
||||
### Selection Logic
|
||||
|
||||
At build time `crypto.config.ts` picks the Noble or native engine for the IETF ChaCha20-Poly1305 primitive; the selector routes every call to the chosen implementation. Authentication tag mismatch throws the shared `CryptoCipherError` with code `CRYPTO.CIPHER.TAG_MISMATCH`. Nonce is caller-supplied; the IETF 12-byte nonce variant is the one implemented here — the extended 24-byte XChaCha20 variant lives in `xchacha20-poly1305`.
|
||||
@@ -0,0 +1,63 @@
|
||||
# chacha20-poly1305.native
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
`chacha20-poly1305.native.ts` is the native-backend placeholder for ChaCha20-Poly1305 in `@suaveplan/crypto-cipher`. It exports `encrypt`, `decrypt`, and the three length constants with the same types as the Noble implementation, but `encrypt` and `decrypt` throw `PrimitiveNotImplemented("chacha20-poly1305")`.
|
||||
|
||||
This stub maintains the two-implementation selector layout for ChaCha20-Poly1305. Unlike AES-GCM, ChaCha20-Poly1305 is not yet available as a native WebCrypto primitive in all environments, but it is available in libsodium and in the `@suaveplan/crypto-sodium` engine adapter. The stub keeps the door open for a platform-specific native binding.
|
||||
|
||||
## Features
|
||||
|
||||
- Exports `encrypt`, `decrypt`, `KEY_LENGTH = 32`, `NONCE_LENGTH = 12`, `TAG_LENGTH = 16`.
|
||||
- `encrypt` and `decrypt` throw `PrimitiveNotImplemented("chacha20-poly1305")`.
|
||||
- Constants are usable even when the cipher functions are not implemented.
|
||||
- Zero runtime dependencies beyond `@suaveplan/crypto-utils/errors`.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
// Active only when crypto.config.ts sets chacha20-poly1305 to 'native'.
|
||||
import { encrypt, KEY_LENGTH, NONCE_LENGTH } from "@suaveplan/crypto-cipher/chacha20-poly1305";
|
||||
|
||||
try {
|
||||
encrypt(new Uint8Array(KEY_LENGTH), new Uint8Array(NONCE_LENGTH), new Uint8Array(8));
|
||||
} catch (err) {
|
||||
// PrimitiveNotImplemented: chacha20-poly1305
|
||||
console.error(err);
|
||||
}
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { KEY_LENGTH, NONCE_LENGTH } from "@suaveplan/crypto-cipher/chacha20-poly1305";
|
||||
|
||||
// Allocate key and nonce buffers regardless of whether cipher functions are available.
|
||||
function allocate(): { key: Uint8Array; nonce: Uint8Array } {
|
||||
return {
|
||||
key: crypto.getRandomValues(new Uint8Array(KEY_LENGTH)),
|
||||
nonce: crypto.getRandomValues(new Uint8Array(NONCE_LENGTH)),
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `encrypt(_key, _nonce, _plaintext, _aad?): Uint8Array`
|
||||
|
||||
Throws `PrimitiveNotImplemented("chacha20-poly1305")`.
|
||||
|
||||
### `decrypt(_key, _nonce, _ciphertext, _aad?): Uint8Array`
|
||||
|
||||
Throws `PrimitiveNotImplemented("chacha20-poly1305")`.
|
||||
|
||||
### Constants
|
||||
|
||||
- `KEY_LENGTH = 32`, `NONCE_LENGTH = 12`, `TAG_LENGTH = 16`
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The constants are defined as literal values (not imported from the Noble file) so both implementations are self-contained. This matches the pattern used by all other native stub files in the package. When a platform-native ChaCha20-Poly1305 binding is ready and RFC 8439 test vectors pass, the placeholder functions are replaced, parameter prefixes removed, and the selector regenerated.
|
||||
@@ -0,0 +1,79 @@
|
||||
# chacha20-poly1305.noble
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
`chacha20-poly1305.noble.ts` is the Noble-backed concrete implementation of ChaCha20-Poly1305 in `@suaveplan/crypto-cipher`. It uses `chacha20poly1305` from `@noble/ciphers/chacha`, validates key and nonce lengths before every operation, and wraps decryption failures in typed `CryptoCipherError` instances.
|
||||
|
||||
ChaCha20-Poly1305 (RFC 8439) combines the ChaCha20 stream cipher with the Poly1305 MAC. The stream cipher generates a keystream that is XOR'd with the plaintext; the MAC provides authentication over both the ciphertext and any additional authenticated data. This file is the production implementation; `chacha20-poly1305.native.ts` is the placeholder.
|
||||
|
||||
## Features
|
||||
|
||||
- `encrypt(key, nonce, plaintext, aad?)` — validates lengths, returns `ciphertext ‖ 16-byte Poly1305 tag`.
|
||||
- `decrypt(key, nonce, ciphertext, aad?)` — validates lengths, verifies tag, throws `CryptoCipherError(DECRYPT_FAILED)` on mismatch.
|
||||
- Exports `KEY_LENGTH = 32`, `NONCE_LENGTH = 12`, `TAG_LENGTH = 16`.
|
||||
- Validation throws `CryptoCipherError(INVALID_KEY_LENGTH)` or `CryptoCipherError(INVALID_NONCE_LENGTH)` with structured context.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { encrypt, decrypt, KEY_LENGTH, NONCE_LENGTH } from "@suaveplan/crypto-cipher/chacha20-poly1305";
|
||||
|
||||
const key = crypto.getRandomValues(new Uint8Array(KEY_LENGTH));
|
||||
const nonce = crypto.getRandomValues(new Uint8Array(NONCE_LENGTH));
|
||||
const pt = new TextEncoder().encode("RFC 8439");
|
||||
|
||||
const ct = encrypt(key, nonce, pt);
|
||||
const recovered = decrypt(key, nonce, ct);
|
||||
console.log(new TextDecoder().decode(recovered)); // "RFC 8439"
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { encrypt, KEY_LENGTH, NONCE_LENGTH } from "@suaveplan/crypto-cipher/chacha20-poly1305";
|
||||
import { CryptoCipherError } from "@suaveplan/crypto-cipher/errors";
|
||||
|
||||
// Encrypt with metadata bound as AAD.
|
||||
function encryptPacket(
|
||||
key: Uint8Array,
|
||||
sequenceNumber: number,
|
||||
payload: Uint8Array
|
||||
): Uint8Array {
|
||||
const nonce = crypto.getRandomValues(new Uint8Array(NONCE_LENGTH));
|
||||
const aad = new Uint8Array(4);
|
||||
new DataView(aad.buffer).setUint32(0, sequenceNumber, false);
|
||||
return new Uint8Array([...nonce, ...encrypt(key, nonce, payload, aad)]);
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `encrypt(key: Uint8Array, nonce: Uint8Array, plaintext: Uint8Array, aad?: Uint8Array): Uint8Array`
|
||||
|
||||
| Parameter | Type | Description |
|
||||
|-----------|------|-------------|
|
||||
| `key` | `Uint8Array` | 32-byte symmetric key. |
|
||||
| `nonce` | `Uint8Array` | 12-byte nonce. Must not be reused with the same key. |
|
||||
| `plaintext` | `Uint8Array` | Data to encrypt. |
|
||||
| `aad` | `Uint8Array` (optional) | Authenticated but not encrypted associated data. |
|
||||
|
||||
**Returns:** `ciphertext ‖ tag` of length `plaintext.length + 16`.
|
||||
|
||||
**Throws:** `CryptoCipherError(INVALID_KEY_LENGTH)` or `CryptoCipherError(INVALID_NONCE_LENGTH)`.
|
||||
|
||||
### `decrypt(key: Uint8Array, nonce: Uint8Array, ciphertext: Uint8Array, aad?: Uint8Array): Uint8Array`
|
||||
|
||||
**Returns:** Decrypted plaintext.
|
||||
|
||||
**Throws:** `CryptoCipherError(DECRYPT_FAILED)` on authentication failure; `CryptoCipherError(INVALID_KEY_LENGTH | INVALID_NONCE_LENGTH)` on length violations.
|
||||
|
||||
### Constants
|
||||
|
||||
- `KEY_LENGTH = 32`, `NONCE_LENGTH = 12`, `TAG_LENGTH = 16`
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
Noble's `chacha20poly1305(key, nonce, aad)` follows the same construction pattern as `gcm`: it returns an object with `.encrypt` and `.decrypt`. The AAD is passed at construction time and bound into the Poly1305 tag computation per RFC 8439 §2.8. ChaCha20 is immune to timing attacks related to data values (unlike AES's S-box lookups on platforms without AES-NI), making it the preferred choice for constant-time environments. RFC 8439 §A.5 test vectors pass in the test suite.
|
||||
@@ -0,0 +1,69 @@
|
||||
# xchacha20-poly1305.native
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
`xchacha20-poly1305.native.ts` is the native-backend placeholder for XChaCha20-Poly1305 in `@suaveplan/crypto-cipher`. It exports `encrypt`, `decrypt`, `KEY_LENGTH`, `NONCE_LENGTH`, and `TAG_LENGTH` with the same types as the Noble implementation, but `encrypt` and `decrypt` throw `PrimitiveNotImplemented("xchacha20-poly1305")`.
|
||||
|
||||
XChaCha20-Poly1305 is available natively in libsodium as `crypto_secretbox_xchacha20poly1305` and is accessible through the `@suaveplan/crypto-sodium` engine. A future native implementation of this placeholder could delegate to libsodium or a Bun native binding. This stub keeps the selector layout complete until that work is ready.
|
||||
|
||||
## Features
|
||||
|
||||
- Exports `encrypt`, `decrypt`, `KEY_LENGTH = 32`, `NONCE_LENGTH = 24`, `TAG_LENGTH = 16`.
|
||||
- `encrypt` and `decrypt` throw `PrimitiveNotImplemented("xchacha20-poly1305")`.
|
||||
- Constants are usable regardless of implementation status.
|
||||
- Zero runtime dependencies beyond `@suaveplan/crypto-utils/errors`.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
// Active only when crypto.config.ts sets xchacha20-poly1305 to 'native'.
|
||||
import { encrypt, KEY_LENGTH, NONCE_LENGTH } from "@suaveplan/crypto-cipher/xchacha20-poly1305";
|
||||
|
||||
try {
|
||||
encrypt(
|
||||
new Uint8Array(KEY_LENGTH),
|
||||
new Uint8Array(NONCE_LENGTH),
|
||||
new Uint8Array(8)
|
||||
);
|
||||
} catch (err) {
|
||||
// PrimitiveNotImplemented: xchacha20-poly1305
|
||||
console.error(err);
|
||||
}
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { KEY_LENGTH, NONCE_LENGTH } from "@suaveplan/crypto-cipher/xchacha20-poly1305";
|
||||
|
||||
// Buffer allocation is safe regardless of which backend is active.
|
||||
function generateKeyMaterial(): { key: Uint8Array; nonce: Uint8Array } {
|
||||
return {
|
||||
key: crypto.getRandomValues(new Uint8Array(KEY_LENGTH)),
|
||||
nonce: crypto.getRandomValues(new Uint8Array(NONCE_LENGTH)),
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `encrypt(_key, _nonce, _plaintext, _aad?): Uint8Array`
|
||||
|
||||
Throws `PrimitiveNotImplemented("xchacha20-poly1305")`.
|
||||
|
||||
### `decrypt(_key, _nonce, _ciphertext, _aad?): Uint8Array`
|
||||
|
||||
Throws `PrimitiveNotImplemented("xchacha20-poly1305")`.
|
||||
|
||||
### Constants
|
||||
|
||||
- `KEY_LENGTH = 32` — 256-bit key.
|
||||
- `NONCE_LENGTH = 24` — 192-bit extended nonce.
|
||||
- `TAG_LENGTH = 16` — 128-bit Poly1305 tag.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The `NONCE_LENGTH = 24` constant is explicitly defined here (not imported from the Noble file) to keep both implementations self-contained. This is consistent with the pattern used across all native stub files in the package. When the native binding is ready and IETF XChaCha20-Poly1305 draft test vectors pass, the placeholder functions are replaced and the selector regenerated. The `PrimitiveNotImplemented` error from `@suaveplan/crypto-utils/errors` is a `SuaveplanError` subclass and participates in the full structured error system including cause-chain traversal and OTel span events.
|
||||
@@ -0,0 +1,77 @@
|
||||
# xchacha20-poly1305.noble
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
`xchacha20-poly1305.noble.ts` is the Noble-backed concrete implementation of XChaCha20-Poly1305 in `@suaveplan/crypto-cipher`. It uses `xchacha20poly1305` from `@noble/ciphers/chacha`, validates key and nonce lengths before every call, and wraps decryption failures in `CryptoCipherError`.
|
||||
|
||||
XChaCha20-Poly1305 derives a subkey from the first 16 bytes of the 24-byte nonce using HChaCha20, then runs ChaCha20-Poly1305 with the remaining 8-byte nonce. This construction allows safe random-nonce generation for very large message volumes. This file is the production implementation; `xchacha20-poly1305.native.ts` is the placeholder.
|
||||
|
||||
## Features
|
||||
|
||||
- `encrypt(key, nonce, plaintext, aad?)` — validates lengths, returns `ciphertext ‖ 16-byte Poly1305 tag`.
|
||||
- `decrypt(key, nonce, ciphertext, aad?)` — validates lengths, verifies tag, throws `CryptoCipherError(DECRYPT_FAILED)` on mismatch.
|
||||
- Exports `KEY_LENGTH = 32`, `NONCE_LENGTH = 24`, `TAG_LENGTH = 16`.
|
||||
- Throws `CryptoCipherError(INVALID_KEY_LENGTH)` or `CryptoCipherError(INVALID_NONCE_LENGTH)` on violations.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { encrypt, decrypt, KEY_LENGTH, NONCE_LENGTH } from "@suaveplan/crypto-cipher/xchacha20-poly1305";
|
||||
|
||||
const key = crypto.getRandomValues(new Uint8Array(KEY_LENGTH));
|
||||
const nonce = crypto.getRandomValues(new Uint8Array(NONCE_LENGTH));
|
||||
const pt = new TextEncoder().encode("hello xchacha");
|
||||
|
||||
const ct = encrypt(key, nonce, pt);
|
||||
const recovered = decrypt(key, nonce, ct);
|
||||
console.log(new TextDecoder().decode(recovered)); // "hello xchacha"
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { encrypt, KEY_LENGTH, NONCE_LENGTH } from "@suaveplan/crypto-cipher/xchacha20-poly1305";
|
||||
|
||||
// Encrypt a record for long-term storage with AAD from record metadata.
|
||||
function encryptRecord(
|
||||
key: Uint8Array,
|
||||
record: Uint8Array,
|
||||
metadataJson: string
|
||||
): { nonce: Uint8Array; ciphertext: Uint8Array } {
|
||||
const nonce = crypto.getRandomValues(new Uint8Array(NONCE_LENGTH));
|
||||
const aad = new TextEncoder().encode(metadataJson);
|
||||
return { nonce, ciphertext: encrypt(key, nonce, record, aad) };
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `encrypt(key: Uint8Array, nonce: Uint8Array, plaintext: Uint8Array, aad?: Uint8Array): Uint8Array`
|
||||
|
||||
| Parameter | Type | Description |
|
||||
|-----------|------|-------------|
|
||||
| `key` | `Uint8Array` | 32-byte key. |
|
||||
| `nonce` | `Uint8Array` | 24-byte extended nonce. |
|
||||
| `plaintext` | `Uint8Array` | Data to encrypt. |
|
||||
| `aad` | `Uint8Array` (optional) | Authenticated but not encrypted data. |
|
||||
|
||||
**Returns:** `ciphertext ‖ tag` of length `plaintext.length + 16`.
|
||||
|
||||
**Throws:** `CryptoCipherError(INVALID_KEY_LENGTH | INVALID_NONCE_LENGTH)`.
|
||||
|
||||
### `decrypt(key: Uint8Array, nonce: Uint8Array, ciphertext: Uint8Array, aad?: Uint8Array): Uint8Array`
|
||||
|
||||
**Returns:** Decrypted plaintext.
|
||||
|
||||
**Throws:** `CryptoCipherError(DECRYPT_FAILED)` on tag mismatch; `CryptoCipherError(INVALID_KEY_LENGTH | INVALID_NONCE_LENGTH)` on length violations.
|
||||
|
||||
### Constants
|
||||
|
||||
- `KEY_LENGTH = 32`, `NONCE_LENGTH = 24`, `TAG_LENGTH = 16`
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
Noble's `xchacha20poly1305(key, nonce, aad)` handles the HChaCha20 subkey derivation step and the subsequent ChaCha20-Poly1305 call transparently. The NONCE_LENGTH of 24 is the only structural difference from `chacha20-poly1305.noble.ts`; the validation logic and error handling are otherwise identical. The extended nonce makes random nonce generation safe for up to approximately 2⁸⁰ messages per key before collision probability becomes non-negligible. Draft test vectors from the IETF Crypto Forum Research Group pass in the test suite.
|
||||
@@ -0,0 +1,73 @@
|
||||
# xchacha20-poly1305
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
- [xchacha20-poly1305.native](./impl/xchacha20-poly1305.native/xchacha20-poly1305.native.md) — <!-- begin:auto-docs --> _(no submodules)_- [xchacha20-poly1305.noble](./impl/xchacha20-poly1305.noble/xchacha20-poly1305.noble.md) — <!-- begin:auto-docs --> _(no submodules)_ - [xchacha20-poly1305.noble](./impl/xchacha20-poly1305.noble/xchacha20-poly1305.noble.md) — <!-- begin:auto-docs --> _(no submodules)_- [xchacha20-poly1305.select](./xchacha20-poly1305.select/xchacha20-poly1305.select.md) — <!-- begin:auto-docs --> _(no submodules)_ - [xchacha20-poly1305.select](./xchacha20-poly1305.select/xchacha20-poly1305.select.md) — <!-- begin:auto-docs --> _(no submodules)_- [xchacha20-poly1305](./xchacha20-poly1305.md) — <!-- begin:auto-docs --> _(no submodules)_- xchacha20-poly1305.native — <!-- begin:auto-docs -->…
|
||||
- [xchacha20-poly1305.native](./impl/xchacha20-poly1305.native/xchacha20-poly1305.native.md) — <!-- begin:auto-docs --> _(no submodules)_- [xchacha20-poly1305.noble](./impl/xchacha20-poly1305.noble/xchacha20-poly1305.noble.md) — <!-- begin:auto-docs --> _(no submodules)_- [xchacha20-poly1305.noble](./impl/xchacha20-poly1305.noble/xchacha20-poly1305.noble.md) — <!-- begin:auto-docs --> _(no submodules)_- [xchacha20-poly1305.select](./xchacha20-poly1305.select/xchacha20-poly1305.select.md) — <!-- begin:auto-docs --> _(no submodules)_- [xchacha20-poly1305.select](./xchacha20-poly1305.select/xchacha20-poly1305.select.md) — <!-- begin:auto-docs --> _(no submodules)_- [xchacha20-poly1305](./xchacha20-poly1305.md) — <!-- begin:auto-docs --> _(no submodules)_- [xchacha20-poly1305.native](./impl/xchacha20-poly1305.native/xchacha20-poly1305.native.md) — <!-- begin:auto-docs --> _(no submodules)_- [xchacha20-poly1305.noble](./impl/xchacha20-poly1305.noble/xchacha20-poly1305.noble.md) — <!-- begin:auto-docs --> _(no submodules)_- [xchacha20-poly1305.noble](./impl/xchacha20-poly1305.noble/xchacha20-poly1305.noble.md) — <!-- begin:auto-docs --> _(no submodules)_- [xchacha20-poly1305.select](./xchacha20-poly1305.select/xchacha20-poly1305.select.md) — <!-- begin:auto-docs --> _(no submodules)_- [xchacha20-poly1305.select](./xchacha20-poly1305.select/xchacha20-poly1305.select.md) — <!-- begin:auto-docs --> _(no submodules)_- [xchacha20-poly1305](./xchacha20-poly1305.md) — <!-- begin:auto-docs --> _(no submodules)_- xchacha20-poly1305.native —…
|
||||
- [xchacha20-poly1305.native](./impl/xchacha20-poly1305.native/xchacha20-poly1305.native.md) — <!-- begin:auto-docs --> _(no submodules)_- [xchacha20-poly1305.noble](./impl/xchacha20-poly1305.noble/xchacha20-poly1305.noble.md) — <!-- begin:auto-docs --> _(no submodules)_- [xchacha20-poly1305.noble](./impl/xchacha20-poly1305.noble/xchacha20-poly1305.noble.md) — <!-- begin:auto-docs --> _(no submodules)_- [xchacha20-poly1305.select](./xchacha20-poly1305.select/xchacha20-poly1305.select.md) — <!-- begin:auto-docs --> _(no submodules)_- [xchacha20-poly1305.select](./xchacha20-poly1305.select/xchacha20-poly1305.select.md) — <!-- begin:auto-docs --> _(no submodules)_- [xchacha20-poly1305](./xchacha20-poly1305.md) — <!-- begin:auto-docs --> _(no submodules)_- xchacha20-poly1305.native — <!-- begin:auto-docs --…
|
||||
- [xchacha20-poly1305.native](./impl/xchacha20-poly1305.native/xchacha20-poly1305.native.md) — <!-- begin:auto-docs --> _(no submodules)_- [xchacha20-poly1305.noble](./impl/xchacha20-poly1305.noble/xchacha20-poly1305.noble.md) — <!-- begin:auto-docs --> _(no submodules)_ - [xchacha20-poly1305.noble](./impl/xchacha20-poly1305.noble/xchacha20-poly1305.noble.md) — <!-- begin:auto-docs --> _(no submodules)_- [xchacha20-poly1305.select](./xchacha20-poly1305.select/xchacha20-poly1305.select.md) — <!-- begin:auto-docs --> _(no submodules)_ - [xchacha20-poly1305.select](./xchacha20-poly1305.select/xchacha20-poly1305.select.md) — <!-- begin:auto-docs --> _(no submodules)_- [xchacha20-poly1305](./xchacha20-poly1305.md) — <!-- begin:auto-docs --> _(no submodules)_
|
||||
## Purpose
|
||||
|
||||
`xchacha20-poly1305.ts` is the stable public entry point for the XChaCha20-Poly1305 AEAD primitive in `@suaveplan/crypto-cipher`. It re-exports from the generated `xchacha20-poly1305.select.ts`, which forwards to the Noble-backed or native implementation based on `crypto.config.ts`.
|
||||
|
||||
XChaCha20-Poly1305 is an extension of ChaCha20-Poly1305 with a 24-byte (192-bit) nonce instead of the standard 12-byte nonce. The extended nonce allows safe random generation at high message volumes — the birthday bound for nonce collision with random 24-byte nonces is approximately 2⁸⁰ messages per key, compared to 2⁴⁸ for 12-byte nonces. It is used in libsodium's `crypto_secretbox_xsalsa20poly1305` and is the default AEAD in protocols like Signal's `@suaveplan/crypto` façade.
|
||||
|
||||
## Features
|
||||
|
||||
- `encrypt(key, nonce, plaintext, aad?)` — AEAD encryption; returns `ciphertext ‖ tag`.
|
||||
- `decrypt(key, nonce, ciphertext, aad?)` — AEAD decryption; throws `CryptoCipherError` on failure.
|
||||
- `KEY_LENGTH = 32` — 256-bit key.
|
||||
- `NONCE_LENGTH = 24` — 192-bit extended nonce (distinguishes from standard ChaCha20-Poly1305).
|
||||
- `TAG_LENGTH = 16` — 128-bit Poly1305 authentication tag.
|
||||
- Key and nonce length validated before every operation.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { encrypt, decrypt, KEY_LENGTH, NONCE_LENGTH } from "@suaveplan/crypto-cipher/xchacha20-poly1305";
|
||||
|
||||
const key = crypto.getRandomValues(new Uint8Array(KEY_LENGTH));
|
||||
const nonce = crypto.getRandomValues(new Uint8Array(NONCE_LENGTH)); // 24 bytes
|
||||
const plaintext = new TextEncoder().encode("xchacha test");
|
||||
|
||||
const ciphertext = encrypt(key, nonce, plaintext);
|
||||
const recovered = decrypt(key, nonce, ciphertext);
|
||||
console.log(new TextDecoder().decode(recovered)); // "xchacha test"
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { encrypt, decrypt, KEY_LENGTH, NONCE_LENGTH } from "@suaveplan/crypto-cipher/xchacha20-poly1305";
|
||||
|
||||
// Store-and-forward encryption with random nonce prepended to ciphertext.
|
||||
function encryptForStorage(key: Uint8Array, plaintext: Uint8Array): Uint8Array {
|
||||
const nonce = crypto.getRandomValues(new Uint8Array(NONCE_LENGTH));
|
||||
const ct = encrypt(key, nonce, plaintext);
|
||||
const out = new Uint8Array(NONCE_LENGTH + ct.length);
|
||||
out.set(nonce, 0);
|
||||
out.set(ct, NONCE_LENGTH);
|
||||
return out;
|
||||
}
|
||||
|
||||
function decryptFromStorage(key: Uint8Array, blob: Uint8Array): Uint8Array {
|
||||
const nonce = blob.slice(0, NONCE_LENGTH);
|
||||
const ct = blob.slice(NONCE_LENGTH);
|
||||
return decrypt(key, nonce, ct);
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
All symbols re-exported from `xchacha20-poly1305.select.ts`:
|
||||
|
||||
- `encrypt(key, nonce, plaintext, aad?): Uint8Array`
|
||||
- `decrypt(key, nonce, ciphertext, aad?): Uint8Array`
|
||||
- `KEY_LENGTH: 32`
|
||||
- `NONCE_LENGTH: 24`
|
||||
- `TAG_LENGTH: 16`
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
This file is a zero-logic re-export barrel. The distinguishing constant is `NONCE_LENGTH = 24`, not 12 as in standard ChaCha20-Poly1305. Prepending the random nonce to the ciphertext is a common pattern for store-and-forward scenarios, as shown in the advanced example. The nonce is not secret and safe to transmit alongside the ciphertext.
|
||||
@@ -0,0 +1,56 @@
|
||||
# xchacha20-poly1305.select
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
`xchacha20-poly1305.select.ts` is the build-time primitive selector for XChaCha20-Poly1305 in `@suaveplan/crypto-cipher`. Generated by `bun scripts/gen-crypto-selects.ts`, it re-exports one backend — `xchacha20-poly1305.noble.ts` or `xchacha20-poly1305.native.ts` — based on the `xchacha20-poly1305` field in `crypto.config.ts`.
|
||||
|
||||
Callers import through the stable `xchacha20-poly1305.ts` entry point. This selector level means backend changes require only regenerating this file; all call sites above it remain unchanged. Currently the Noble backend is selected.
|
||||
|
||||
## Features
|
||||
|
||||
- Re-exports `encrypt`, `decrypt`, `KEY_LENGTH`, `NONCE_LENGTH`, `TAG_LENGTH`.
|
||||
- Generated — never hand-edited.
|
||||
- Controlled by `crypto.config.ts`: set `xchacha20-poly1305` to `'noble'` or `'native'`.
|
||||
- Enables dead-code elimination of the inactive backend.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { KEY_LENGTH, NONCE_LENGTH } from "@suaveplan/crypto-cipher/xchacha20-poly1305";
|
||||
|
||||
console.log(KEY_LENGTH); // 32
|
||||
console.log(NONCE_LENGTH); // 24 (XChaCha20's extended nonce)
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { encrypt, decrypt, NONCE_LENGTH } from "@suaveplan/crypto-cipher/xchacha20-poly1305";
|
||||
|
||||
// Round-trip with random nonce.
|
||||
function roundTrip(key: Uint8Array, msg: Uint8Array): Uint8Array {
|
||||
const nonce = crypto.getRandomValues(new Uint8Array(NONCE_LENGTH));
|
||||
return decrypt(key, nonce, encrypt(key, nonce, msg));
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
Re-exports from the active implementation:
|
||||
|
||||
- `encrypt(key: Uint8Array, nonce: Uint8Array, plaintext: Uint8Array, aad?: Uint8Array): Uint8Array`
|
||||
- `decrypt(key: Uint8Array, nonce: Uint8Array, ciphertext: Uint8Array, aad?: Uint8Array): Uint8Array`
|
||||
- `KEY_LENGTH: 32`
|
||||
- `NONCE_LENGTH: 24`
|
||||
- `TAG_LENGTH: 16`
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
This file is a generated one-liner marked `/* GENERATED — DO NOT EDIT */`. The `NONCE_LENGTH` of 24 is the defining characteristic of XChaCha20 relative to standard ChaCha20. The generator resolves the `xchacha20-poly1305` backend from `crypto.config.ts` and emits the correct `export *` path. Regenerating is idempotent.
|
||||
|
||||
### Selection Logic
|
||||
|
||||
At build time `crypto.config.ts` picks the Noble or native engine for XChaCha20-Poly1305 — the 24-byte-nonce variant suitable for random-nonce encryption of large numbers of messages under a single key. The selector routes every call to the chosen impl and throws `PrimitiveNotImplemented` when the selected engine has no native binding yet. Nonce is caller-supplied in every path.
|
||||
@@ -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