first commit

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

View File

@@ -0,0 +1,25 @@
# `crypto`
Cryptographic primitives, ciphers, hashes, curves, and key management.
<!-- begin:auto-docs -->
## Packages in `crypto`
13 packages.
| Package | Description |
|---|---|
| [`@suaveplan/crypto`](./crypto/README.md) | Universal cryptographic primitives — engine-pluggable AEAD, Ed25519/ECDSA-P256 signing, X25519 key agreement, sealed-box, and auth-box. Built on audited @noble… |
| [`@suaveplan/crypto-bin`](./crypto-bin/README.md) | Suaveplan password-hashing CLI. Ships the `suaveplan-hash-password` binary — Argon2id (default) and BLAKE2b password hashing + verification with PHC/MCF-format… |
| [`@suaveplan/crypto-browser`](./crypto-browser/README.md) | WebCrypto-backed engine implementations for `@suaveplan/crypto` — AES-256-GCM, Ed25519, ECDSA-P256/P384, X25519, ECDH-P256/P384, RSA, AES-KW, and non-extractab… |
| [`@suaveplan/crypto-cipher`](./crypto-cipher/README.md) | Low-level AEAD (Authenticated Encryption with Associated Data) cipher primitives for the `@suaveplan/crypto` stack. Provides three algorithms — AES-256-GCM, Ch… |
| [`@suaveplan/crypto-cli`](./crypto-cli/README.md) | Suaveplan keychain CLI — root + server identity lifecycle (init, roll, revoke, inspect, verify, export-public, restore) |
| [`@suaveplan/crypto-core`](./crypto-core/README.md) | Universal cryptographic primitives — engine-pluggable AEAD, Ed25519/ECDSA-P256 signing, X25519 key agreement, sealed-box, and auth-box. Built on audited @noble… |
| [`@suaveplan/crypto-curve`](./crypto-curve/README.md) | Low-level elliptic-curve building blocks for the `@suaveplan/crypto` stack. Exposes four curves — secp256k1, Ed25519, X25519, and NIST P-256 — each as a standa… |
| [`@suaveplan/crypto-hash`](./crypto-hash/README.md) | Pure-TypeScript hashing substrate. SHA-2 digests, HMAC, HKDF, PBKDF2, Argon2id, and hex/Base64 encoding — no WASM, no browser globals, fully tree-shakeable. |
| [`@suaveplan/crypto-keys`](./crypto-keys/README.md) | Hierarchical keychain chain-signing for the crypto ecosystem. Store-agnostic, free of `node:` / `bun:` imports. |
| [`@suaveplan/crypto-rsa`](./crypto-rsa/README.md) | RSA primitives for Suaveplan — RFC 8017 PKCS #1 v2.2, RFC 5208 / RFC 5958 PKCS #8, RFC 5280 SPKI, RFC 7517 / RFC 7518 JWK, RFC 7468 PEM. Universal package (zer… |
| [`@suaveplan/crypto-sodium`](./crypto-sodium/README.md) | libsodium-backed engine implementations for `@suaveplan/crypto`. Provides AEAD encryption, Ed25519 signing, X25519 key agreement, sealed-box, auth-box, determi… |
| [`@suaveplan/crypto-utils`](./crypto-utils/README.md) | Shared cryptographic utilities consumed internally by all other `@suaveplan/crypto-*` packages. Provides the low-level primitives that every crypto implementat… |
| [`@suaveplan/crypto-vault`](./crypto-vault/README.md) | Cryptographic foundation for the Vaulted password-manager ecosystem. Provides BIP39 mnemonic generation, OWASP-grade Argon2id derivation, two-secret key deriva… |
<!-- end:auto-docs -->

View File

@@ -0,0 +1 @@
Cryptographic primitives, ciphers, hashes, curves, and key management.

View File

@@ -0,0 +1,105 @@
# @suaveplan/crypto-bin - Suaveplan crypto bins: suaveplan-hash-password (Argon2id / blake2b password hash + verify CLI). Lives in its own package so the universal @suaveplan/crypto does not pull in @suaveplan/cli runtime deps.
## Summary
Suaveplan password-hashing CLI. Ships the `suaveplan-hash-password`
binary — Argon2id (default) and BLAKE2b password hashing + verification
with PHC/MCF-formatted output strings, distinct exit codes per failure
class, and an env-injected output adapter so the core is testable
without subprocess spawn.
This package lives separately from `@suaveplan/crypto` so the
universal crypto façade does not pull CLI runtime deps into
browser/server consumers that just want primitives.
<!-- begin:auto-docs -->
## Documentation tree
- [bin](./src/bin/hash-password.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [hash-password](./src/bin/hash-password.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [hash-password](./src/hash-password/hash-password.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [hash-password](./src/hash-password/hash-password.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
<!-- end:auto-docs --> - [hash-password](./src/bin/hash-password.md) — <!-- begin:auto-docs --> _(no submodules)_- [hash-password](./src/hash-password/hash-password.md) — <!-- begin:auto-docs --> _(no submodules)_- [hash-password](./src/hash-password/hash-password.md) — <!-- begin:auto-docs --> _(no submodules)_- [hash-password](./src/bin/hash-password.md) — <!-- begin:auto-docs --> _(no submodules)_- [hash-password](./src/hash-password/hash-password.md) — <!-- begin:auto-docs --> _(no submodules)_- [hash-password](./src/hash-password/hash-password.md) — <!-- begin:auto-docs --> _(no submodules)_- [hash-password](./src/bin/hash-password.md) — <!-- begin:auto-docs --> _(no submodules)_- [hash-password](./src/hash-password/hash-password.md) — <!-- begin:auto-docs --> _(no submodules)_- [hash-password](./src/hash-password/hash-password.md) — <!-- begin:auto-docs --> _(no submodules)_- [hash-password](./src/bin/hash-password.md) — <!-- begin:auto-docs --> _(no submodules)_- [hash-password](./src/hash-password/hash-password.md) — <!-- begin:auto-docs --> _(no submodules)_ - [hash-password](./src/hash-password/hash-password.md) — <!-- begin:auto-docs --> _(no submodules)_
## Quickstart
```bash
# Hash a password (Argon2id by default)
$ suaveplan-hash-password --password "correct horse battery staple"
$argon2id$v=19$m=65536,t=3,p=1$<salt>$<hash>
# Verify (exit 0 = match, 1 = mismatch)
$ suaveplan-hash-password --password "correct horse battery staple" \
--verify '$argon2id$v=19$m=65536,t=3,p=1$<salt>$<hash>'
$ echo $?
0
```
## Algorithms
- **`--algo argon2id`** (default) — OWASP 2023-recommended password hash.
Output prefix `$argon2id$v=19$...`.
- **`--algo blake2b`** — opt-in alternative for environments that need
it. Output prefix `$blake2b$v=1$...`. Cost factor is configurable via
`--cost-factor <n>` (1–31).
The verify mode auto-detects the algorithm from the stored hash's prefix,
so callers do not have to track which algorithm was used at hash time.
## Exit codes
| Code | Meaning |
|------|------------------------------------------------------------|
| 0 | Success / verify matched |
| 1 | Verify mismatch (wrong password) |
| 2 | Bad arguments (missing/malformed flag value) |
| 3 | Unknown algorithm (unrecognised `--algo` or stored prefix) |
| 4 | Verify failed (stored hash is corrupt or unparseable) |
The `0` / `1` split matches the de-facto contract for shell pipelines —
`if suaveplan-hash-password --password ... --verify ...; then ...; fi`
works as expected, with `3+` reserved for operator errors so callers can
distinguish "wrong password" from "stored data is broken".
## Programmatic use
```ts
import { main } from "@suaveplan/crypto-bin";
const out: string[] = [];
const err: string[] = [];
const code = main(
["--password", "pw", "--algo", "blake2b", "--cost-factor", "10"],
{
write: (s) => out.push(s),
writeError: (s) => err.push(s),
},
);
if (code === 0) {
storeHash(out.join("").trim());
}
```
The `main()` function never throws — every error path is captured and
surfaced via `env.writeError(...)` and a non-zero return value. See
`src/hash-password/hash-password.md` for the full contract.
## Public surface
- `main` — testable CLI core.
- `parseArgs` — pure argv parser (throws `HashPasswordCliError`).
- `detectAlgo` — algorithm sniffer for stored hashes.
- `HashPasswordCliEnv` — output adapter interface.
- `HashPasswordCliError` — error class wrapping the stable code registry.
- `HASH_PASSWORD_CLI_CODES` — `BAD_ARGS` / `UNKNOWN_ALGO` /
`VERIFY_FAILED`.
## Dependencies
- `@suaveplan/crypto-hash` — Argon2id + BLAKE2b primitives.
- `@suaveplan/error` — `SuaveplanError` base + stable code registry.
- `@suaveplan/types` — shared cross-package contracts.
No dependency on `@suaveplan/cli` — argv parsing is hand-rolled (~40
lines) so the bin stays small.

View File

@@ -0,0 +1,107 @@
# `suaveplan-hash-password` bin entry
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
This file is the platform-runtime entry for the `suaveplan-hash-password`
binary shipped by `@suaveplan/crypto-bin`. It is intentionally a minimal
shim: it constructs an `HashPasswordCliEnv` adapter from `process.stdout`
and `process.stderr`, then delegates to the testable `main()` function in
`../hash-password/hash-password.ts`. All argument parsing, algorithm
dispatch, and exit-code logic live in that pure core so they can be unit
tested by direct invocation rather than by spawning a subprocess.
The shim exists so the package can publish a Node-style `bin` entry under
`./dist/bin/hash-password.js` (with the `#!/usr/bin/env node` shebang
preserved by Vite) without coupling the testable core to `process.*`
globals. Anything that touches `process.argv` / `process.exit` /
`process.stdout` is confined to this module.
## Features
- Forwards `process.argv.slice(2)` into `main()` and exits with the
returned numeric code.
- Wires `process.stdout.write` / `process.stderr.write` into the
`HashPasswordCliEnv` adapter — the core never references `process`
directly.
- Auto-exec guard: only runs `main()` when invoked as the top-level
module (`process.argv[1]` ends with `hash-password.js`). Importing this
module from a test or another script does not trigger an exit.
- Coverage-excluded by design — this module is exercised end-to-end via
the bin install path; the logic it wraps is fully covered by
`../hash-password/hash-password.test.ts`.
- Shebang line emitted verbatim into the dist bundle so the file is
directly executable once symlinked by `bun install` / `npm install`.
## Usage: Basic Example
```bash
# Once installed, the bin is on PATH via the package.json `bin` map
$ suaveplan-hash-password --password "correct horse battery staple"
$argon2id$v=19$m=65536,t=3,p=1$<salt>$<hash>
$ echo $?
0
```
## Usage: Advanced Example
```bash
# Pipe-friendly verify (exit 0 = match, 1 = mismatch, 3+ = error)
$ STORED='$argon2id$v=19$m=65536,t=3,p=1$<salt>$<hash>'
$ if suaveplan-hash-password --password "$PW" --verify "$STORED"; then
> echo "login ok"
> else
> case $? in
> 1) echo "wrong password" ;;
> 3) echo "stored hash has unrecognised algorithm" ;;
> 4) echo "stored hash is corrupt" ;;
> *) echo "argument error" ;;
> esac
> fi
```
For programmatic invocation that does not spawn a process, import the
testable core directly instead of this shim:
```ts
import { main } from "@suaveplan/crypto-bin";
const code = main(["--password", "pw"], {
write: (s) => process.stdout.write(s),
writeError: (s) => process.stderr.write(s),
});
```
## API Reference
This module has no public exports — it is a side-effect entry point.
Behaviour:
- On import, registers the auto-exec guard and (if invoked as the
top-level script) calls `main(argv, env)` and exits with its return
value.
- `argv`: `process.argv.slice(2)` — same shape `main()` accepts.
- `env.write(s)`: forwards `s` to `process.stdout.write` synchronously.
- `env.writeError(s)`: forwards `s` to `process.stderr.write`
synchronously.
For the contract of `main()` itself (exit codes, accepted flags, error
classes) see `../hash-password/hash-password.md`.
## Implementation Notes
The auto-exec guard checks `process.argv[1]?.endsWith("hash-password.js")`
rather than `import.meta.url === ...` so the same module survives Vite's
ES-module bundling without ESM-vs-CJS surprises in older Node hosts. The
guard string matches the bundled filename (`hash-password.js`) deliberately
— the source is `.ts` and is never executed directly in production.
`/* istanbul ignore */` markers wrap the guard and the `process.exit`
call because the test suite invokes `main()` directly with a captured env
adapter (see `../hash-password/hash-password.test.ts`). Spawning a
subprocess to cover this two-line wrapper would add filesystem and Node
runtime coupling to the test suite without exercising any logic that is
not already covered. The `vitest.config.ts` coverage `exclude` list
documents this decision in a sibling comment so future readers do not
mistake the exclusion for an oversight.

View File

@@ -0,0 +1,63 @@
# suaveplan-hash-password CLI
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`hash-password.ts` is the testable core of the `suaveplan-hash-password` binary in `@suaveplan/crypto-bin`. It accepts an argv-style list of flags, an env adapter (so tests can capture stdout/stderr without process side effects), and returns an exit code. Two modes: hash mode (no `--verify`) prints a stored hash; verify mode (`--verify <stored>`) checks a candidate password against a stored hash and returns 0/1 by match outcome.
The bin lives in its own package (`@suaveplan/crypto-bin`) rather than alongside the universal `@suaveplan/crypto` because CLI surfaces would force the entire `@suaveplan/cli` runtime tree into universal consumers; keeping the bin in a separate package preserves universal purity.
## Features
- Two algorithms: `argon2id` (default, OWASP 2023 recommended) and `blake2b` (opt-in via `--algo blake2b`).
- Hash mode: prints `$argon2id$v=19$...` or `$blake2b$v=1$...` to stdout, exits 0.
- Verify mode: detects algorithm from the stored hash's prefix and runs the matching `verifyPassword*` function. Exits 0 on match, 1 on mismatch, 3+ on argument / format errors.
- Strict argument validation: missing `--password`, unknown `--algo`, malformed `--cost-factor` all surface as `HashPasswordCliError` with one of three stable error codes (`HASH_PASSWORD_CLI_BAD_ARGS` / `HASH_PASSWORD_CLI_UNKNOWN_ALGO` / `HASH_PASSWORD_CLI_VERIFY_FAILED`).
- Env-injected stdout/stderr — never touches `process.stdout` / `process.stderr` directly, so tests can capture output without spawning a subprocess.
- Per-spec exit-code contract: 0 = success / verify-match, 1 = verify-mismatch, 2 = bad args, 3 = unknown algo, 4 = verify failed (corrupt stored hash).
## Usage: Basic Example
```bash
# Hash a password (Argon2id by default)
$ suaveplan-hash-password --password "correct horse battery staple"
$argon2id$v=19$m=65536,t=3,p=1$<salt>$<hash>
# Verify
$ suaveplan-hash-password --password "correct horse battery staple" --verify '$argon2id$v=19$m=65536,t=3,p=1$<salt>$<hash>'
$ echo $?
0
```
## Usage: Advanced Example
```ts
// Programmatic invocation (e.g. from a test harness or a higher-level CLI)
import { main } from "@suaveplan/crypto-bin/hash-password/hash-password";
const out: string[] = [];
const err: string[] = [];
const code = main(["--password", "pw", "--algo", "blake2b", "--cost-factor", "10"], {
write: (s) => out.push(s),
writeError: (s) => err.push(s),
});
if (code === 0) {
storeHash(out.join("").trim());
}
```
## API Reference
- `main(argv: readonly string[], env: HashPasswordCliEnv): number` — testable entry point.
- `parseArgs(argv: readonly string[]): HashPasswordCliArgs` — pure argv parser; throws `HashPasswordCliError` on bad input.
- `detectAlgo(storedHash: string): Algo` — algorithm sniffer; throws on unknown prefix.
- `HashPasswordCliEnv { write(s), writeError(s) }` — output adapter interface.
- `HashPasswordCliError extends SuaveplanError` — error class wrapping the three stable codes.
- `HASH_PASSWORD_CLI_CODES` — exported registry: `HASH_PASSWORD_CLI_BAD_ARGS`, `HASH_PASSWORD_CLI_UNKNOWN_ALGO`, `HASH_PASSWORD_CLI_VERIFY_FAILED`.
## Implementation Notes
The CLI deliberately does not depend on `@suaveplan/cli` to avoid pulling the readline / TTY-prompt machinery into a tiny one-shot binary. argv parsing is hand-rolled (≈40 lines) because the option set is small and stable. Verify-mode exit codes are spec-pinned: 0 = match, 1 = mismatch — those two values are the contract for shell pipelines (`if suaveplan-hash-password --password ... --verify ...; then login; fi`). Errors from the verify path (corrupt stored hash) intentionally exit with `4` rather than `1` so a caller can distinguish "wrong password" from "your stored hash is corrupt"; the spec's `verify mismatch exit 1` requirement is preserved for the wrong-password case.

View File

@@ -0,0 +1,16 @@
# @suaveplan/crypto-browser Changelog
## 0.0.0 — suaveplan initial
- Initial implementation: WebCrypto-backed AEAD, sign, and agree engines
- `webCryptoAeadEngine` — AES-256-GCM via `crypto.subtle`
- `webCryptoSignEngine` / `webCryptoEd25519Engine` / `webCryptoEcdsaP256Engine` — Ed25519 and ECDSA-P256
- `webCryptoAgreeEngine` / `webCryptoX25519Engine` / `webCryptoEcdhP256Engine` — X25519 and ECDH-P256
- `rsa` namespace — RSA-PSS, RSA-OAEP, RSASSA-PKCS1-v1_5
- `aesKw` namespace — AES Key Wrap (RFC 3394)
- `ecdsaP384` namespace — ECDSA with P-384 / SHA-384
- `ecdhP384` namespace — ECDH with P-384
- `nonExtractableKey()` — non-extractable CryptoKey generation
- `isWebCryptoAvailable()` / `assertWebCryptoAvailable()` — runtime checks
- `BrowserCryptoError` with codes: `NOT_AVAILABLE`, `KEY.GENERATION_FAILED`, `OPERATION.FAILED`
- 100% coverage threshold (statements, branches, functions, lines)

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,106 @@
# aes-kw
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Provides AES Key Wrap (AES-KW, RFC 3394) operations via `crypto.subtle`. AES-KW is an authenticated key-wrapping algorithm that encapsulates cryptographic key material with integrity protection, producing output that is 8 bytes longer than the input. It is widely used in key management systems, JOSE (JSON Web Encryption), CMS enveloped-data, and TLS session ticket encryption.
Unlike general-purpose AES-GCM, AES-KW is specifically designed for wrapping other cryptographic keys. The algorithm provides integrity verification — any tampering with the wrapped output will be detected during unwrapping.
## Features
- `wrapKey(wrappingKey, keyToWrap)` — wraps raw key bytes using AES-KW via `crypto.subtle.wrapKey`
- `unwrapKey(wrappingKey, wrappedKey)` — unwraps and returns raw key bytes via `crypto.subtle.unwrapKey`
- Supports 128, 192, and 256-bit (16, 24, 32-byte) wrapping keys
- Input key must be a multiple of 8 bytes (AES-KW requirement)
- Wrapped output is input length + 8 (integrity check value)
- `assertWebCryptoAvailable()` guard on every call
- Typed `BrowserCryptoError` on all failure paths
## Usage: Basic Example
```ts
import { aesKw } from "@suaveplan/crypto-browser/aes-kw/aes-kw";
const wrappingKey = crypto.getRandomValues(new Uint8Array(32)); // 256-bit KEK
const aesKey = crypto.getRandomValues(new Uint8Array(32)); // 256-bit content key
const wrapped = await aesKw.wrapKey(wrappingKey, aesKey);
// wrapped.length === aesKey.length + 8 === 40
const recovered = await aesKw.unwrapKey(wrappingKey, wrapped);
// recovered deepEquals aesKey
```
## Usage: Advanced Example
```ts
import { aesKw } from "@suaveplan/crypto-browser/aes-kw/aes-kw";
import { webCryptoAeadEngine } from "@suaveplan/crypto-browser/engines/webcrypto-aead-engine";
import { BrowserCryptoError } from "@suaveplan/crypto-browser/errors";
// Key Encryption Key (KEK) derived from a user password or stored securely
declare const kek: Uint8Array; // 32-byte wrapping key
// Wrap content encryption key for storage or transmission
async function storeWrappedKey(contentKey: Uint8Array): Promise<Uint8Array> {
try {
return await aesKw.wrapKey(kek, contentKey);
} catch (err) {
if (err instanceof BrowserCryptoError && err.code === "BROWSER_CRYPTO.OPERATION.FAILED") {
throw new Error("Key wrapping failed — check wrapping key size");
}
throw err;
}
}
// Later: unwrap and use the content key
async function loadAndEncrypt(
wrappedKey: Uint8Array,
plaintext: Uint8Array
): Promise<Uint8Array> {
const contentKey = await aesKw.unwrapKey(kek, wrappedKey);
const nonce = crypto.getRandomValues(new Uint8Array(12));
return webCryptoAeadEngine.encrypt(contentKey, plaintext, nonce) as unknown as Promise<Uint8Array>;
}
```
## API Reference
### `aesKw.wrapKey(wrappingKey, keyToWrap): Promise<Uint8Array>`
Wraps `keyToWrap` using AES-KW.
| Parameter | Type | Description |
|-----------|------|-------------|
| `wrappingKey` | `Uint8Array` | 16, 24, or 32-byte AES Key Encryption Key (KEK) |
| `keyToWrap` | `Uint8Array` | Key bytes to wrap; must be a multiple of 8 bytes |
**Returns:** `Promise<Uint8Array>` — wrapped bytes, `keyToWrap.length + 8` bytes long.
**Throws:** `BrowserCryptoError` — `BROWSER_CRYPTO.NOT_AVAILABLE` or `BROWSER_CRYPTO.OPERATION.FAILED`.
---
### `aesKw.unwrapKey(wrappingKey, wrappedKey): Promise<Uint8Array>`
Unwraps `wrappedKey` using AES-KW and returns the original key bytes.
| Parameter | Type | Description |
|-----------|------|-------------|
| `wrappingKey` | `Uint8Array` | 16, 24, or 32-byte KEK (must match wrapping key) |
| `wrappedKey` | `Uint8Array` | Previously wrapped bytes from `wrapKey()` |
**Returns:** `Promise<Uint8Array>` — original raw key bytes.
**Throws:** `BrowserCryptoError` — `BROWSER_CRYPTO.OPERATION.FAILED` when integrity check fails (wrong key or tampered data).
## Implementation Notes
`crypto.subtle.wrapKey` requires a `CryptoKey` object as input (not a raw `Uint8Array`). The implementation imports `keyToWrap` as a temporary `AES-GCM` key with `extractable: true` before wrapping — this is the minimal ceremony needed to satisfy the `crypto.subtle` API. The temporary key is never exported directly; only the AES-KW wrapped form is returned.
Similarly, `crypto.subtle.unwrapKey` produces a `CryptoKey` internally; the implementation immediately re-exports it as raw bytes via `exportKey("raw", ...)` so the caller receives a portable `Uint8Array`. This round-trip through `CryptoKey` is required by the Web Crypto API and adds negligible overhead.
AES-KW does not use a nonce — it is deterministic for a given (wrapping-key, key-to-wrap) pair. This is intentional for RFC 3394 compliance; nonce-based confidentiality is provided by AES-GCM when encrypting arbitrary data.

View File

@@ -0,0 +1,85 @@
# availability
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Provides a runtime check for the presence of the Web Crypto API (`crypto.subtle`). The check is a prerequisite for every operation in `@suaveplan/crypto-browser`: all engine methods and primitives call `assertWebCryptoAvailable()` before attempting any `crypto.subtle` call, so callers receive a structured, typed `BrowserCryptoError` rather than an opaque `TypeError: Cannot read properties of undefined (reading 'subtle')`.
The two exported helpers form a small contract: `isWebCryptoAvailable` is a pure predicate useful for conditional branching or feature-flag evaluation, while `assertWebCryptoAvailable` is the guard variant used internally throughout the package.
## Features
- `isWebCryptoAvailable(): boolean` — returns `true` when both `globalThis.crypto` and `globalThis.crypto.subtle` are defined; otherwise `false`
- `assertWebCryptoAvailable(): void` — throws `BrowserCryptoError` with code `BROWSER_CRYPTO.NOT_AVAILABLE` if the check fails
- Zero side effects — purely reads globals, never modifies them
- Works across Node 20+, Bun, Deno, and modern browsers without polyfills
## Usage: Basic Example
```ts
import { isWebCryptoAvailable } from "@suaveplan/crypto-browser/availability/availability";
if (!isWebCryptoAvailable()) {
console.warn("WebCrypto not available — using fallback cipher");
}
```
## Usage: Advanced Example
```ts
import { assertWebCryptoAvailable } from "@suaveplan/crypto-browser/availability/availability";
import { BrowserCryptoError } from "@suaveplan/crypto-browser/errors";
async function encryptUserData(key: Uint8Array, data: Uint8Array): Promise<Uint8Array> {
// Guard before any crypto.subtle calls
assertWebCryptoAvailable();
// ... proceed with crypto.subtle.encrypt
return new Uint8Array();
}
// Caller can catch the specific structured error
try {
await encryptUserData(key, data);
} catch (err) {
if (err instanceof BrowserCryptoError && err.code === "BROWSER_CRYPTO.NOT_AVAILABLE") {
// Fall back to a server-side encryption endpoint
}
}
```
## API Reference
### `isWebCryptoAvailable(): boolean`
Returns `true` when `typeof crypto !== 'undefined' && typeof crypto.subtle !== 'undefined'`.
| Parameter | Type | Description |
|-----------|------|-------------|
| _(none)_ | — | — |
**Returns:** `boolean` — `true` if `crypto.subtle` is accessible.
**Throws:** never.
---
### `assertWebCryptoAvailable(): void`
Calls `isWebCryptoAvailable()` and throws if the result is `false`.
| Parameter | Type | Description |
|-----------|------|-------------|
| _(none)_ | — | — |
**Returns:** `void`
**Throws:** `BrowserCryptoError` — code `BROWSER_CRYPTO.NOT_AVAILABLE` when `crypto.subtle` is absent.
## Implementation Notes
The check deliberately uses `typeof crypto !== 'undefined'` (rather than a `try/catch` or optional chaining on a possibly-unset global) to avoid triggering a `ReferenceError` in environments where `crypto` is not declared at all. Optional chaining `globalThis?.crypto?.subtle` is not used because `globalThis` itself might be unavailable in some older embedded runtimes.
Both exports are side-effect-free and tree-shakeable: `"sideEffects": false` is set on the package, so bundlers that import only `isWebCryptoAvailable` will not include `BrowserCryptoError` in the output unless it is actually used.

View File

@@ -0,0 +1,105 @@
# ecdh-p384
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Provides Elliptic Curve Diffie-Hellman key agreement using the NIST P-384 curve via `crypto.subtle`. P-384 ECDH delivers 192 bits of security, matching the security level of AES-256 and making it appropriate for long-term key agreement in high-security contexts. This module is the P-384 analogue of the `webCryptoEcdhP256Engine` provided in the engines module, exposed as a standalone namespace for direct use outside of the `AgreeEngine` interface.
Key agreement produces a 48-byte (384-bit) shared secret by calling `crypto.subtle.deriveBits` with `length: 384`. Callers are responsible for passing this secret through a KDF before using it as an encryption key.
## Features
- `generateKeypair()` — generates a P-384 ECDH keypair via `crypto.subtle.generateKey`
- `agree(privateKey, publicKey)` — derives 48-byte shared secret via `crypto.subtle.deriveBits`
- Full DH symmetry: `agree(alice, bob.pub)` equals `agree(bob, alice.pub)`
- Public key: 97-byte uncompressed point (raw format, 0x04 prefix)
- Private key: PKCS8 DER bytes
- `assertWebCryptoAvailable()` guard on every call
- Typed `BrowserCryptoError` for all failure paths
## Usage: Basic Example
```ts
import { ecdhP384 } from "@suaveplan/crypto-browser/ecdh-p384/ecdh-p384";
const alice = await ecdhP384.generateKeypair();
const bob = await ecdhP384.generateKeypair();
// Both sides independently compute the same 48-byte secret
const aliceShared = await ecdhP384.agree(alice.privateKey, bob.publicKey);
const bobShared = await ecdhP384.agree(bob.privateKey, alice.publicKey);
// aliceShared deepEquals bobShared — 48 bytes (384 bits)
```
## Usage: Advanced Example
```ts
import { ecdhP384 } from "@suaveplan/crypto-browser/ecdh-p384/ecdh-p384";
// KDF using HKDF to derive a 32-byte AES key from the P-384 shared secret
async function deriveAesKey(
privateKey: Uint8Array,
peerPublicKey: Uint8Array,
info: Uint8Array
): Promise<Uint8Array> {
const sharedSecret = await ecdhP384.agree(privateKey, peerPublicKey);
// Import shared secret as HKDF key material
const hkdfKey = await crypto.subtle.importKey(
"raw",
sharedSecret,
"HKDF",
false,
["deriveBits"]
);
const aesKeyBits = await crypto.subtle.deriveBits(
{
name: "HKDF",
hash: "SHA-384",
salt: new Uint8Array(48), // zero salt
info,
},
hkdfKey,
256
);
return new Uint8Array(aesKeyBits);
}
```
## API Reference
### `ecdhP384.generateKeypair(): Promise<{ publicKey: Uint8Array; privateKey: Uint8Array }>`
Generates a P-384 ECDH keypair.
**Returns:** `Promise<{ publicKey: Uint8Array; privateKey: Uint8Array }>` — 97-byte uncompressed public key and PKCS8 DER private key.
**Throws:** `BrowserCryptoError` — `BROWSER_CRYPTO.KEY.GENERATION_FAILED` or `BROWSER_CRYPTO.NOT_AVAILABLE`.
---
### `ecdhP384.agree(privateKey, publicKey): Promise<Uint8Array>`
Performs ECDH key agreement, deriving 384 bits from `deriveBits`.
| Parameter | Type | Description |
|-----------|------|-------------|
| `privateKey` | `Uint8Array` | PKCS8 DER private key bytes |
| `publicKey` | `Uint8Array` | 97-byte uncompressed P-384 public key |
**Returns:** `Promise<Uint8Array>` — 48-byte (384-bit) shared secret.
**Throws:** `BrowserCryptoError` — `BROWSER_CRYPTO.OPERATION.FAILED` or `BROWSER_CRYPTO.NOT_AVAILABLE`.
## Implementation Notes
P-384 `deriveBits` is called with `length: 384` to extract the full 384-bit shared secret. This matches the curve's security level and avoids the truncation that would occur if 256 bits were requested. Callers using the output as an AES-256 key should pass through HKDF first to properly condition the key material.
The public key is exported from `crypto.subtle` via `"raw"` format, which produces the standard uncompressed point encoding. This is directly importable on any Web Crypto implementation that supports P-384.
P-384 is approximately 2–3× slower than P-256 for key generation and agreement. For applications where P-256's 128-bit security is sufficient, `webCryptoEcdhP256Engine` provides the same API with better performance.

View File

@@ -0,0 +1,117 @@
# ecdsa-p384
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Provides ECDSA (Elliptic Curve Digital Signature Algorithm) operations using the NIST P-384 curve and SHA-384 hash, backed by `crypto.subtle`. P-384 offers 192 bits of security — stronger than P-256's 128 bits — and is required by many compliance frameworks (NIST SP 800-57, CNSA Suite 2.0, NSA Suite B). This module is intended for contexts where P-384 is mandated by policy or interoperability requirements.
The public key format is the 97-byte uncompressed point representation (0x04 prefix followed by 48-byte X and Y coordinates). The private key is PKCS8 DER. Both formats are standard and directly importable by other Web Crypto implementations.
## Features
- `generateKeypair()` — generates a P-384 ECDSA keypair using `crypto.subtle.generateKey`
- `sign(privateKey, message)` — signs with ECDSA-P384 / SHA-384
- `verify(publicKey, signature, message)` — verifies an ECDSA-P384 signature
- Public key: 97-byte uncompressed point (raw format)
- Private key: PKCS8 DER bytes
- Signature format: IEEE P1363 (R || S) as returned by `crypto.subtle`
- `assertWebCryptoAvailable()` guard on every call
- Typed `BrowserCryptoError` for all failure paths
## Usage: Basic Example
```ts
import { ecdsaP384 } from "@suaveplan/crypto-browser/ecdsa-p384/ecdsa-p384";
const kp = await ecdsaP384.generateKeypair();
// kp.publicKey is 97 bytes (uncompressed P-384 point)
// kp.privateKey is PKCS8 DER bytes
const message = new TextEncoder().encode("document to sign");
const signature = await ecdsaP384.sign(kp.privateKey, message);
const valid = await ecdsaP384.verify(kp.publicKey, signature, message);
console.log(valid); // true
```
## Usage: Advanced Example
```ts
import { ecdsaP384 } from "@suaveplan/crypto-browser/ecdsa-p384/ecdsa-p384";
import { BrowserCryptoError } from "@suaveplan/crypto-browser/errors";
// Issue a signed certificate-like structure
async function issueAttestation(
issuerPrivateKey: Uint8Array,
payload: Uint8Array
): Promise<{ signature: Uint8Array; algorithm: string }> {
try {
const signature = await ecdsaP384.sign(issuerPrivateKey, payload);
return { signature, algorithm: "ECDSA-P384-SHA384" };
} catch (err) {
if (err instanceof BrowserCryptoError) {
throw new Error(`Signing failed: ${err.code}`);
}
throw err;
}
}
// Verify the attestation with a stored public key
async function verifyAttestation(
publicKey: Uint8Array,
payload: Uint8Array,
signature: Uint8Array
): Promise<boolean> {
return ecdsaP384.verify(publicKey, signature, payload);
}
```
## API Reference
### `ecdsaP384.generateKeypair(): Promise<{ publicKey: Uint8Array; privateKey: Uint8Array }>`
Generates a P-384 ECDSA keypair.
**Returns:** `Promise<{ publicKey: Uint8Array; privateKey: Uint8Array }>` — 97-byte uncompressed public key and PKCS8 DER private key.
**Throws:** `BrowserCryptoError` — `BROWSER_CRYPTO.KEY.GENERATION_FAILED` or `BROWSER_CRYPTO.NOT_AVAILABLE`.
---
### `ecdsaP384.sign(privateKey, message): Promise<Uint8Array>`
Signs `message` using ECDSA-P384 with SHA-384.
| Parameter | Type | Description |
|-----------|------|-------------|
| `privateKey` | `Uint8Array` | PKCS8 DER private key bytes |
| `message` | `Uint8Array` | Bytes to sign |
**Returns:** `Promise<Uint8Array>` — IEEE P1363 signature (96 bytes for P-384).
**Throws:** `BrowserCryptoError` — `BROWSER_CRYPTO.OPERATION.FAILED` or `BROWSER_CRYPTO.NOT_AVAILABLE`.
---
### `ecdsaP384.verify(publicKey, signature, message): Promise<boolean>`
Verifies an ECDSA-P384 / SHA-384 signature.
| Parameter | Type | Description |
|-----------|------|-------------|
| `publicKey` | `Uint8Array` | 97-byte uncompressed P-384 public key |
| `signature` | `Uint8Array` | IEEE P1363 signature bytes |
| `message` | `Uint8Array` | Original message bytes |
**Returns:** `Promise<boolean>` — `true` if signature is valid.
**Throws:** `BrowserCryptoError` — `BROWSER_CRYPTO.OPERATION.FAILED` or `BROWSER_CRYPTO.NOT_AVAILABLE`.
## Implementation Notes
SHA-384 is paired with P-384 because the security level of the hash should match that of the curve. Using SHA-256 with P-384 would create a mismatch: the 192-bit curve security would be limited to the 128-bit collision resistance of SHA-256.
The signature output format from `crypto.subtle.sign` for ECDSA is IEEE P1363 (raw R || S concatenation), not DER/ASN.1. This differs from OpenSSL's default DER encoding. When interoperating with systems that expect DER signatures, a conversion layer is needed.
P-384 key generation is typically 2–5× slower than P-256 due to the larger field size. For high-throughput signing workloads where P-256 security is sufficient, consider using `webCryptoEcdsaP256Engine` instead.

View File

@@ -0,0 +1,108 @@
# webcrypto-aead-engine
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Provides an `AeadEngine` implementation that delegates all AES-256-GCM operations to the browser's (or Node's) native `crypto.subtle` API. This lets `@suaveplan/crypto` facade functions — `encrypt()` and `decrypt()` from the `aead` sub-module — run on hardware-accelerated paths wherever the Web Crypto API is available, instead of the default pure-JavaScript `@noble/ciphers` path.
The engine is designed for environments where minimising bundle size matters most: importing it adds zero third-party cryptography code because everything is provided by the host runtime.
## Features
- `encrypt(key, plaintext, nonce, aad?)` — AES-256-GCM encryption via `crypto.subtle.encrypt`, including automatic 16-byte authentication tag appended to ciphertext
- `decrypt(key, ciphertext, nonce, aad?)` — AES-256-GCM decryption via `crypto.subtle.decrypt` with authenticated tag verification
- Optional additional authenticated data (AAD) for binding ciphertext to a context
- Throws typed `BrowserCryptoError` with stable codes for every failure path
- Returns `Promise<Uint8Array>` — engine methods are async even though the `AeadEngine` interface declares sync return types; facade functions accept Promise results
- Guards every call with `assertWebCryptoAvailable()` to produce a structured error instead of a runtime `TypeError`
## Usage: Basic Example
```ts
import { webCryptoAeadEngine } from "@suaveplan/crypto-browser/engines/webcrypto-aead-engine";
const key = crypto.getRandomValues(new Uint8Array(32));
const nonce = crypto.getRandomValues(new Uint8Array(12));
const plaintext = new TextEncoder().encode("hello world");
const ciphertext = await (webCryptoAeadEngine.encrypt(key, plaintext, nonce) as unknown as Promise<Uint8Array>);
const recovered = await (webCryptoAeadEngine.decrypt(key, ciphertext, nonce) as unknown as Promise<Uint8Array>);
console.log(new TextDecoder().decode(recovered)); // "hello world"
```
## Usage: Advanced Example
```ts
import { encrypt } from "@suaveplan/crypto/aead";
import { webCryptoAeadEngine } from "@suaveplan/crypto-browser/engines/webcrypto-aead-engine";
import { BrowserCryptoError } from "@suaveplan/crypto-browser/errors";
async function sealPayload(
key: Uint8Array,
payload: Uint8Array,
context: Uint8Array // AAD binds the ciphertext to e.g. a session ID
): Promise<Uint8Array> {
try {
const nonce = crypto.getRandomValues(new Uint8Array(12));
const sealed = await (webCryptoAeadEngine.encrypt(key, payload, nonce, context) as unknown as Promise<Uint8Array>);
// Prepend nonce for transport: [12 bytes nonce | ciphertext+tag]
const out = new Uint8Array(12 + sealed.length);
out.set(nonce, 0);
out.set(sealed, 12);
return out;
} catch (err) {
if (err instanceof BrowserCryptoError) {
// err.code is one of: BROWSER_CRYPTO.NOT_AVAILABLE | BROWSER_CRYPTO.OPERATION.FAILED
throw err;
}
throw err;
}
}
```
## API Reference
### `webCryptoAeadEngine: AeadEngine`
An object conforming to the `AeadEngine` interface from `@suaveplan/crypto/engines`.
#### `.encrypt(key, plaintext, nonce, aad?): Uint8Array`
Encrypts plaintext using AES-256-GCM.
| Parameter | Type | Description |
|-----------|------|-------------|
| `key` | `Uint8Array` | 32-byte (256-bit) AES key |
| `plaintext` | `Uint8Array` | Bytes to encrypt |
| `nonce` | `Uint8Array` | 12-byte IV (use a fresh random nonce per message) |
| `aad` | `Uint8Array \| undefined` | Optional additional authenticated data |
**Returns:** `Uint8Array` (runtime: `Promise<Uint8Array>`) — ciphertext with 16-byte GCM tag appended.
**Throws:** `BrowserCryptoError` — `BROWSER_CRYPTO.NOT_AVAILABLE` or `BROWSER_CRYPTO.OPERATION.FAILED`.
#### `.decrypt(key, ciphertext, nonce, aad?): Uint8Array`
Decrypts ciphertext and verifies the GCM authentication tag.
| Parameter | Type | Description |
|-----------|------|-------------|
| `key` | `Uint8Array` | 32-byte AES key (must match encryption key) |
| `ciphertext` | `Uint8Array` | Ciphertext with 16-byte GCM tag appended |
| `nonce` | `Uint8Array` | 12-byte IV used during encryption |
| `aad` | `Uint8Array \| undefined` | Optional AAD (must match encryption AAD exactly) |
**Returns:** `Uint8Array` (runtime: `Promise<Uint8Array>`) — decrypted plaintext.
**Throws:** `BrowserCryptoError` — `BROWSER_CRYPTO.OPERATION.FAILED` on authentication failure or bad key.
## Implementation Notes
`crypto.subtle.importKey` is called on every encrypt/decrypt invocation rather than being cached, because raw `Uint8Array` keys are the idiomatic interface in `AeadEngine` and caching a `CryptoKey` across calls would require a `Map<Uint8Array, CryptoKey>` with careful lifecycle management. The overhead of `importKey` is dominated by the actual AES-GCM operation for typical payload sizes.
The `AeadEngine` interface uses synchronous return types (`Uint8Array`), while `crypto.subtle` is entirely asynchronous. The implementation returns `Promise<Uint8Array>` cast to `Uint8Array` at the type level. Facade functions in `@suaveplan/crypto` use `await` on engine results, so this is transparent to consumers using those facades.
The 12-byte nonce is the NIST-recommended IV length for AES-GCM and provides the best performance with `crypto.subtle`. Longer IVs (96–128 bits) would work but require internal counter splitting which is not done here — callers must supply exactly a 12-byte nonce for maximum compatibility.

View File

@@ -0,0 +1,118 @@
# webcrypto-agree-engine
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Provides two `AgreeEngine` implementations — `webCryptoX25519Engine` (X25519 Diffie-Hellman) and `webCryptoEcdhP256Engine` (ECDH with P-256) — both backed by the native `crypto.subtle` API. A convenience alias `webCryptoAgreeEngine` resolves to the X25519 engine. These engines implement the `AgreeEngine` interface from `@suaveplan/crypto/engines`, making them drop-in substitutes for the default `@noble/curves`-backed engine in environments where `crypto.subtle` is available.
Key agreement is the foundation of forward-secrecy protocols (TLS, Noise, Signal). Using `crypto.subtle` for ECDH ensures hardware acceleration is used when the runtime supports it, and the key material never leaves the key management subsystem if non-extractable keys are used.
## Features
- `generateKeypair()` — generates an ephemeral X25519 or ECDH-P256 keypair; returns portable raw bytes
- `agree(mySecret, theirPublic)` — derives a 32-byte shared secret via `crypto.subtle.deriveBits`
- Full DH symmetry: `agree(alice, bob.pub) === agree(bob, alice.pub)` for any keypair pair
- All methods are async at runtime despite `AgreeEngine` sync declarations
- `assertWebCryptoAvailable()` guard on every call
- Typed `BrowserCryptoError` on generation or agreement failure
## Usage: Basic Example
```ts
import { webCryptoX25519Engine } from "@suaveplan/crypto-browser/engines/webcrypto-agree-engine";
const alice = await (webCryptoX25519Engine.generateKeypair() as unknown as Promise<{
publicKey: Uint8Array;
secretKey: Uint8Array;
}>);
const bob = await (webCryptoX25519Engine.generateKeypair() as unknown as Promise<{
publicKey: Uint8Array;
secretKey: Uint8Array;
}>);
const aliceSecret = await (webCryptoX25519Engine.agree(alice.secretKey, bob.publicKey) as unknown as Promise<Uint8Array>);
const bobSecret = await (webCryptoX25519Engine.agree(bob.secretKey, alice.publicKey) as unknown as Promise<Uint8Array>);
// aliceSecret deepEquals bobSecret — both parties derived the same 32 bytes
```
## Usage: Advanced Example
```ts
import { webCryptoX25519Engine } from "@suaveplan/crypto-browser/engines/webcrypto-agree-engine";
import { webCryptoAeadEngine } from "@suaveplan/crypto-browser/engines/webcrypto-aead-engine";
// Simple Diffie-Hellman key exchange followed by AES-GCM encryption
async function ephemeralEncrypt(
recipientPublicKey: Uint8Array,
plaintext: Uint8Array
): Promise<{ ephemeralPublic: Uint8Array; ciphertext: Uint8Array }> {
const ephemeral = await (webCryptoX25519Engine.generateKeypair() as unknown as Promise<{
publicKey: Uint8Array;
secretKey: Uint8Array;
}>);
const sharedSecret = await (webCryptoX25519Engine.agree(
ephemeral.secretKey,
recipientPublicKey
) as unknown as Promise<Uint8Array>);
const nonce = crypto.getRandomValues(new Uint8Array(12));
const ciphertext = await (webCryptoAeadEngine.encrypt(
sharedSecret,
plaintext,
nonce
) as unknown as Promise<Uint8Array>);
const out = new Uint8Array(12 + ciphertext.length);
out.set(nonce, 0);
out.set(ciphertext, 12);
return { ephemeralPublic: ephemeral.publicKey, ciphertext: out };
}
```
## API Reference
### `webCryptoX25519Engine: AgreeEngine`
X25519 engine via `crypto.subtle` with `{ name: "X25519" }`.
#### `.generateKeypair(): AgreeKeypair`
Returns `Promise<AgreeKeypair>` at runtime. Public key is 32 raw bytes. Secret key is 32-byte scalar.
**Throws:** `BrowserCryptoError` — `BROWSER_CRYPTO.NOT_AVAILABLE` or `BROWSER_CRYPTO.KEY.GENERATION_FAILED`.
#### `.agree(mySecret, theirPublic): Uint8Array`
Returns `Promise<Uint8Array>` (32 bytes) at runtime.
| Parameter | Type | Description |
|-----------|------|-------------|
| `mySecret` | `Uint8Array` | 32-byte X25519 private scalar |
| `theirPublic` | `Uint8Array` | 32-byte X25519 public key |
**Throws:** `BrowserCryptoError` — `BROWSER_CRYPTO.NOT_AVAILABLE` or `BROWSER_CRYPTO.OPERATION.FAILED`.
---
### `webCryptoEcdhP256Engine: AgreeEngine`
ECDH-P256 engine via `crypto.subtle`. Public key is 65-byte uncompressed point. Secret key is PKCS8 DER. `agree()` returns 32 bytes via `deriveBits(..., 256)`.
---
### `webCryptoAgreeEngine: AgreeEngine`
Alias for `webCryptoX25519Engine`.
## Implementation Notes
X25519 public keys are exported from `crypto.subtle` in SPKI format and the raw 32-byte scalar is extracted from the last 32 bytes of the DER envelope. On import the scalar is re-wrapped with a minimal 48-byte PKCS8 structure. This keeps the external representation simple (just 32 bytes) while satisfying `crypto.subtle`'s requirement for DER-encoded keys.
For ECDH-P256 the key agreement produces a 256-bit shared secret by calling `deriveBits` with `length: 256`. The raw shared secret should be passed through a KDF (e.g., HKDF) before use as an encryption key; this is not done inside the engine so callers retain full control over key derivation.
X25519 requires Node 20+, Bun 1.0+, or Chrome 113+/Firefox 130+/Safari 15.4+. Older environments will receive `BROWSER_CRYPTO.KEY.GENERATION_FAILED`.

View File

@@ -0,0 +1,119 @@
# webcrypto-sign-engine
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Provides two `SignEngine` implementations — `webCryptoEd25519Engine` (Ed25519) and `webCryptoEcdsaP256Engine` (ECDSA with P-256) — both backed by `crypto.subtle`. The module also exports a convenience alias `webCryptoSignEngine` that resolves to the Ed25519 engine. These engines implement the `SignEngine` interface from `@suaveplan/crypto/engines`, making them drop-in replacements for the default `@noble/curves`-backed engine wherever `crypto.subtle` is available.
Ed25519 provides faster key generation, smaller signatures (64 bytes), and stronger security properties for most use-cases. ECDSA-P256 is provided for interoperability with systems that require NIST curves.
## Features
- `generateKeypair()` — generates an ephemeral Ed25519 or ECDSA-P256 keypair; returns raw public key bytes and raw/PKCS8 private key bytes
- `sign(secretKey, message)` — signs an arbitrary byte array using `crypto.subtle.sign`
- `verify(publicKey, signature, message)` — verifies a signature using `crypto.subtle.verify`
- All methods are async at runtime despite the `SignEngine` interface's sync declarations
- All methods guard with `assertWebCryptoAvailable()` before any `crypto.subtle` calls
- Typed `BrowserCryptoError` thrown on generation failures and operation failures
## Usage: Basic Example
```ts
import { webCryptoEd25519Engine } from "@suaveplan/crypto-browser/engines/webcrypto-sign-engine";
const kp = await (webCryptoEd25519Engine.generateKeypair() as unknown as Promise<{
publicKey: Uint8Array;
secretKey: Uint8Array;
}>);
const message = new TextEncoder().encode("hello");
const signature = await (webCryptoEd25519Engine.sign(kp.secretKey, message) as unknown as Promise<Uint8Array>);
const valid = await (webCryptoEd25519Engine.verify(kp.publicKey, signature, message) as unknown as Promise<boolean>);
console.log(valid); // true
```
## Usage: Advanced Example
```ts
import {
webCryptoEcdsaP256Engine,
webCryptoEd25519Engine,
} from "@suaveplan/crypto-browser/engines/webcrypto-sign-engine";
import { BrowserCryptoError } from "@suaveplan/crypto-browser/errors";
// Use P-256 for JWS / JOSE interoperability
async function signJwsPayload(payload: Uint8Array): Promise<{ publicKey: Uint8Array; signature: Uint8Array }> {
try {
const kp = await (webCryptoEcdsaP256Engine.generateKeypair() as unknown as Promise<{
publicKey: Uint8Array;
secretKey: Uint8Array;
}>);
const signature = await (webCryptoEcdsaP256Engine.sign(kp.secretKey, payload) as unknown as Promise<Uint8Array>);
return { publicKey: kp.publicKey, signature };
} catch (err) {
if (err instanceof BrowserCryptoError) {
// Structured error with err.code = "BROWSER_CRYPTO.KEY.GENERATION_FAILED"
// or "BROWSER_CRYPTO.OPERATION.FAILED"
throw err;
}
throw err;
}
}
```
## API Reference
### `webCryptoEd25519Engine: SignEngine`
Ed25519 engine via `crypto.subtle` with `{ name: "Ed25519" }` algorithm.
#### `.generateKeypair(): SignKeypair`
Returns `Promise<SignKeypair>` at runtime. Public key is 32 raw bytes. Secret key is the 32-byte Ed25519 seed.
**Throws:** `BrowserCryptoError` — `BROWSER_CRYPTO.NOT_AVAILABLE` or `BROWSER_CRYPTO.KEY.GENERATION_FAILED`.
#### `.sign(secretKey, message): Uint8Array`
Returns `Promise<Uint8Array>` at runtime. Signature is 64 bytes.
| Parameter | Type | Description |
|-----------|------|-------------|
| `secretKey` | `Uint8Array` | 32-byte Ed25519 seed |
| `message` | `Uint8Array` | Arbitrary bytes to sign |
**Throws:** `BrowserCryptoError` — `BROWSER_CRYPTO.NOT_AVAILABLE` or `BROWSER_CRYPTO.OPERATION.FAILED`.
#### `.verify(publicKey, signature, message): boolean`
Returns `Promise<boolean>` at runtime.
| Parameter | Type | Description |
|-----------|------|-------------|
| `publicKey` | `Uint8Array` | 32-byte Ed25519 public key |
| `signature` | `Uint8Array` | 64-byte signature |
| `message` | `Uint8Array` | Original message bytes |
**Throws:** `BrowserCryptoError` — `BROWSER_CRYPTO.NOT_AVAILABLE` or `BROWSER_CRYPTO.OPERATION.FAILED`.
---
### `webCryptoEcdsaP256Engine: SignEngine`
ECDSA-P256 engine via `crypto.subtle` with `{ name: "ECDSA", namedCurve: "P-256" }`. Same method signatures as above. Public key is 65-byte uncompressed (0x04 prefix). Secret key is PKCS8 DER.
---
### `webCryptoSignEngine: SignEngine`
Alias for `webCryptoEd25519Engine`. Use this when you want the recommended default.
## Implementation Notes
Key export: `crypto.subtle.exportKey("pkcs8", privateKey)` returns the PKCS8 DER-encoded private key. For Ed25519, the 32-byte seed is located at byte offset 16 within the DER envelope; this is extracted to produce the compact secret key stored and transported by callers. On import, the seed is re-wrapped into the minimal 48-byte PKCS8 ASN.1 structure before calling `crypto.subtle.importKey`.
For ECDSA-P256, public keys are exported via `"raw"` format (65-byte uncompressed point), which is directly importable via `"raw"` as well. This avoids SPKI round-trip complexity.
Ed25519 support requires Node 20+, Bun 1.0+, or a modern browser (Chrome 113+, Firefox 129+, Safari 17+). Calling these engines in older environments will produce a `BrowserCryptoError.KEY.GENERATION_FAILED` from the `crypto.subtle.generateKey` rejection.

View File

@@ -0,0 +1,91 @@
# browser-crypto-error
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Defines `BrowserCryptoError`, the structured error class for all `@suaveplan/crypto-browser` failure paths. It extends `SuaveplanError<BrowserCryptoCode>` from `@suaveplan/error`, inheriting cause-chain traversal, OTel span-event adapter support, and wire-format serialization. Every thrown error in this package is an instance of `BrowserCryptoError` with a stable `BrowserCryptoCode` string code, enabling callers to discriminate failure modes with type-safe `instanceof` + `err.code` checks.
The error class follows the project convention: a package-scoped error with a typed code union, `cause`, `context`, and `timestamp` fields. This makes crypto failures observable, loggable, and serializable without depending on runtime-specific error formats.
## Features
- Extends `SuaveplanError<BrowserCryptoCode>` — participates in all project error infrastructure
- `code` is one of the stable `BROWSER_CRYPTO.*` codes from `codes.ts`
- `cause` captures the underlying `DOMException` or `Error` from `crypto.subtle`
- `context` carries optional structured diagnostic data
- `timestamp` records when the error was thrown
- Stacktrace captured via `Error.captureStackTrace` where available (V8/Node)
## Usage: Basic Example
```ts
import { BrowserCryptoError } from "@suaveplan/crypto-browser/errors";
try {
await webCryptoAeadEngine.decrypt(wrongKey, ciphertext, nonce);
} catch (err) {
if (err instanceof BrowserCryptoError) {
console.error(err.code); // "BROWSER_CRYPTO.OPERATION.FAILED"
console.error(err.cause); // DOMException: The operation failed ...
}
}
```
## Usage: Advanced Example
```ts
import { BrowserCryptoError, BROWSER_CRYPTO_CODES } from "@suaveplan/crypto-browser/errors";
function isNotAvailable(err: unknown): boolean {
return (
err instanceof BrowserCryptoError &&
err.code === BROWSER_CRYPTO_CODES["NOT_AVAILABLE"]
);
}
async function encryptOrFallback(
key: Uint8Array,
data: Uint8Array
): Promise<Uint8Array> {
try {
return await (webCryptoAeadEngine.encrypt(key, data, nonce) as unknown as Promise<Uint8Array>);
} catch (err) {
if (isNotAvailable(err)) {
// Fall back to @noble/ciphers-backed implementation
return nobleAeadFallback(key, data);
}
throw err;
}
}
```
## API Reference
### `class BrowserCryptoError extends SuaveplanError<BrowserCryptoCode>`
**Constructor:**
```ts
new BrowserCryptoError(code: BrowserCryptoCode, messageKey: string, options?: SuaveplanErrorOptions)
```
| Parameter | Type | Description |
|-----------|------|-------------|
| `code` | `BrowserCryptoCode` | Stable error code, e.g. `"BROWSER_CRYPTO.NOT_AVAILABLE"` |
| `messageKey` | `string` | i18n key for the human-readable message |
| `options` | `SuaveplanErrorOptions` | Optional `{ cause, context, traceId, clock }` |
**Inherited properties:**
- `code: BrowserCryptoCode` — stable error code
- `cause?: Error` — underlying cause
- `context?: ErrorContext` — structured diagnostic data
- `timestamp: number` — `Date.now()` at throw time
## Implementation Notes
The constructor is declared `public` despite mirroring the protected base — Biome lint would flag a "useless constructor" but the intent is to widen visibility. The `biome-ignore` comment is present to document this intent.
All `crypto.subtle` rejections produce `DOMException` instances. These are captured as the `cause` field, making the original platform error available for debugging while presenting a structured `BrowserCryptoError` to callers.

View File

@@ -0,0 +1,81 @@
# BROWSER_CRYPTO_CODES
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`codes.ts` defines the complete set of stable error codes used throughout `@suaveplan/crypto-browser`. Each code is a namespaced string constant produced by `defineCodes` from `@suaveplan/error`, ensuring codes are unique within the `"BROWSER_CRYPTO"` namespace and that the `BrowserCryptoCode` union type is always in sync with the registered values at compile time. All WebCrypto-related failures in this package surface under one of these codes, allowing consumers to branch deterministically without parsing message strings.
## Features
- Three codes covering the main WebCrypto failure domains: API unavailability, key generation, and general operation failures
- Generated via `defineCodes("BROWSER_CRYPTO", [...])` which prefixes each entry and freezes the resulting object
- `BrowserCryptoCode` is derived as a `typeof` union for compile-time exhaustive narrowing
- No runtime dependencies beyond `@suaveplan/error`
## Usage: Basic Example
```typescript
import { BROWSER_CRYPTO_CODES } from "@suaveplan/crypto-browser/errors/codes";
import { BrowserCryptoError } from "@suaveplan/crypto-browser/errors/browser-crypto-error";
throw new BrowserCryptoError(
BROWSER_CRYPTO_CODES["NOT_AVAILABLE"],
"error.crypto-browser.not-available"
);
```
## Usage: Advanced Example
```typescript
import {
BROWSER_CRYPTO_CODES,
type BrowserCryptoCode,
} from "@suaveplan/crypto-browser/errors/codes";
import { BrowserCryptoError } from "@suaveplan/crypto-browser/errors/browser-crypto-error";
function handleBrowserCryptoError(err: BrowserCryptoError): string {
switch (err.code as BrowserCryptoCode) {
case BROWSER_CRYPTO_CODES["NOT_AVAILABLE"]:
return "WebCrypto API is not available in this environment (requires HTTPS or localhost).";
case BROWSER_CRYPTO_CODES["KEY.GENERATION_FAILED"]:
return "Key generation failed — the requested algorithm may not be supported by this browser.";
case BROWSER_CRYPTO_CODES["OPERATION.FAILED"]:
return "Cryptographic operation failed — check key type, algorithm, and input size.";
default:
return "Unknown browser crypto error.";
}
}
```
## API Reference
### `BROWSER_CRYPTO_CODES`
```typescript
const BROWSER_CRYPTO_CODES: Readonly<Record<string, string>>
```
A frozen map from short code keys to their namespaced string values. All values are prefixed with `"BROWSER_CRYPTO."` by `defineCodes`.
| Key | Namespaced value | When thrown |
|------------------------|----------------------------------------------|-------------------------------------------------------------------|
| `NOT_AVAILABLE` | `"BROWSER_CRYPTO.NOT_AVAILABLE"` | `globalThis.crypto.subtle` is `undefined` or `null` |
| `KEY.GENERATION_FAILED`| `"BROWSER_CRYPTO.KEY.GENERATION_FAILED"` | `SubtleCrypto.generateKey` or `importKey` rejects |
| `OPERATION.FAILED` | `"BROWSER_CRYPTO.OPERATION.FAILED"` | Any `SubtleCrypto` operation (`encrypt`, `decrypt`, `sign`, `verify`, `deriveBits`, `wrapKey`, `unwrapKey`) rejects |
### `BrowserCryptoCode`
```typescript
type BrowserCryptoCode =
(typeof BROWSER_CRYPTO_CODES)[keyof typeof BROWSER_CRYPTO_CODES];
```
Union of all namespaced code strings. Use this type to annotate `err.code` when narrowing `BrowserCryptoError` in typed catch blocks.
## Implementation Notes
`defineCodes` returns a plain frozen object and the `BrowserCryptoCode` type collapses to a string literal union. Adding a new entry to the array automatically widens the union without requiring any manual type maintenance.
`OPERATION.FAILED` intentionally covers a broad set of `SubtleCrypto` rejections rather than splitting each operation into its own code. The distinction between, say, a failed `encrypt` and a failed `sign` is captured in the structured `context` field on the thrown `BrowserCryptoError` rather than in the code itself, which keeps the code registry small and stable across algorithm additions.

View File

@@ -0,0 +1,97 @@
# non-extractable-key
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Provides `nonExtractableKey()`, a helper that generates a `CryptoKey` with `extractable: false`. Key material generated this way never leaves the cryptographic engine — it cannot be read by JavaScript code via `crypto.subtle.exportKey`. The key handle can be used for cryptographic operations but its raw bytes are permanently inaccessible, providing a hardware-security-module-like boundary in software.
This is the correct approach whenever keys must remain hardware-bound or compliance requirements prohibit key export. Examples include session-scoped encryption keys, FIDO2-style signing keys, and keys derived from hardware-backed key material in PWA contexts. The non-extractability also means that if an adversary gains script execution access, they cannot exfiltrate the key bytes — they can only use the key handle for operations it was created for.
## Features
- Generates a `CryptoKey` with `extractable: false` for any `crypto.subtle`-supported algorithm
- Supports symmetric algorithms: AES-GCM, AES-CBC, AES-KW, HMAC
- Supports asymmetric algorithms: ECDSA (P-256, P-384), ECDH (P-256, P-384), Ed25519, X25519
- For asymmetric algorithms, returns only the private key (the non-extractable half)
- `assertWebCryptoAvailable()` guard before any `crypto.subtle` call
- Typed `BrowserCryptoError` on generation failure
## Usage: Basic Example
```ts
import { nonExtractableKey } from "@suaveplan/crypto-browser/non-extractable-key/non-extractable-key";
// Generate a non-extractable AES-256-GCM key for session encryption
const sessionKey = await nonExtractableKey(
{ name: "AES-GCM", length: 256 },
["encrypt", "decrypt"]
);
// sessionKey.extractable === false
// crypto.subtle.exportKey("raw", sessionKey) will throw
const nonce = crypto.getRandomValues(new Uint8Array(12));
const ciphertext = await crypto.subtle.encrypt(
{ name: "AES-GCM", iv: nonce },
sessionKey,
new TextEncoder().encode("private data")
);
```
## Usage: Advanced Example
```ts
import { nonExtractableKey } from "@suaveplan/crypto-browser/non-extractable-key/non-extractable-key";
// Hardware-bound ECDSA signing key — private key material never leaves the engine
async function createBoundSigningKey(): Promise<CryptoKey> {
return nonExtractableKey(
{ name: "ECDSA", namedCurve: "P-256" },
["sign"]
);
}
// Use the key for signing without any risk of key extraction
async function signChallenge(
key: CryptoKey,
challenge: Uint8Array
): Promise<Uint8Array> {
const sig = await crypto.subtle.sign(
{ name: "ECDSA", hash: "SHA-256" },
key,
challenge
);
return new Uint8Array(sig);
}
// Store the key in IndexedDB for persistence (CryptoKey is structured-cloneable)
// async function storeKey(db: IDBDatabase, key: CryptoKey): Promise<void> { ... }
```
## API Reference
### `nonExtractableKey(algorithm, usages): Promise<CryptoKey>`
Generates a non-extractable `CryptoKey`.
| Parameter | Type | Description |
|-----------|------|-------------|
| `algorithm` | `AlgorithmIdentifier \| RsaHashedKeyGenParams \| EcKeyGenParams \| HmacKeyGenParams \| AesKeyGenParams` | Key generation algorithm descriptor |
| `usages` | `KeyUsage[]` | Intended key operations, e.g. `["encrypt", "decrypt"]` |
**Returns:** `Promise<CryptoKey>` with `extractable === false`.
- For symmetric algorithms: returns the `CryptoKey` directly.
- For asymmetric algorithms: returns the `privateKey` from the generated `CryptoKeyPair`.
**Throws:** `BrowserCryptoError` — `BROWSER_CRYPTO.KEY.GENERATION_FAILED` or `BROWSER_CRYPTO.NOT_AVAILABLE`.
## Implementation Notes
The implementation calls `crypto.subtle.generateKey` with `extractable: false`. The Web Crypto API guarantees that once a key is created non-extractable, no subsequent API call can extract the raw bytes — `exportKey` will throw `InvalidAccessError` unconditionally.
For asymmetric algorithms, `generateKey` returns a `CryptoKeyPair`. The function detects this via `"privateKey" in result` and returns only the private key. The public key is discarded; if callers need the public key they should use the specific engine modules (`ecdsaP384.generateKeypair()`, etc.) which export both.
Non-extractable keys are structured-cloneable and can be persisted to `IndexedDB`, shared with `BroadcastChannel`, or transferred to `Worker` contexts via `postMessage` — without ever exposing the raw key bytes. This makes them suitable for long-lived application keys that survive page reloads.

View File

@@ -0,0 +1,138 @@
# rsa
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Provides a high-level `rsa` namespace object wrapping RSA cryptographic operations from `crypto.subtle`. Supports three algorithm variants — `RSA-PSS`, `RSA-OAEP`, and `RSASSA-PKCS1-v1_5` — for signing/verification and asymmetric encryption/decryption. Keys are represented as standard DER byte arrays (SPKI for public keys, PKCS8 for private keys), which are universally portable across platforms and easy to store and transmit.
RSA is provided alongside the elliptic-curve engines for interoperability with legacy systems, PKI infrastructure, and protocols that require RSA (e.g., certain TLS cipher suites, JWT RSA algorithms, legacy JOSE). For new designs, Ed25519 and ECDH-X25519 are preferred.
## Features
- `generateKeypair(algorithm, modulusLength?)` — generates an RSA keypair; default 2048-bit modulus, configurable up to 4096 bits
- `sign(algorithm, privateKey, message)` — RSA-PSS or RSASSA-PKCS1-v1_5 signature generation
- `verify(algorithm, publicKey, signature, message)` — RSA signature verification
- `encrypt(publicKey, plaintext)` — RSA-OAEP encryption (public-key encryption)
- `decrypt(privateKey, ciphertext)` — RSA-OAEP decryption
- SHA-256 hash used for all operations
- RSA-PSS uses 32-byte salt length
- All methods guard with `assertWebCryptoAvailable()`
- Typed `BrowserCryptoError` for all failure paths
## Usage: Basic Example
```ts
import { rsa } from "@suaveplan/crypto-browser/rsa/rsa";
// Generate and use RSA-PSS for signing
const { publicKey, privateKey } = await rsa.generateKeypair("RSA-PSS");
const message = new TextEncoder().encode("document to sign");
const signature = await rsa.sign("RSA-PSS", privateKey, message);
const valid = await rsa.verify("RSA-PSS", publicKey, signature, message);
console.log(valid); // true
```
## Usage: Advanced Example
```ts
import { rsa, type RsaAlgorithm } from "@suaveplan/crypto-browser/rsa/rsa";
import { BrowserCryptoError } from "@suaveplan/crypto-browser/errors";
// RSA-OAEP for encrypting a small payload (e.g., a symmetric key)
async function wrapAesKey(
recipientPublicKey: Uint8Array,
aesKey: Uint8Array // must be <= ~190 bytes for RSA-2048
): Promise<Uint8Array> {
try {
return await rsa.encrypt(recipientPublicKey, aesKey);
} catch (err) {
if (err instanceof BrowserCryptoError) {
console.error(`Crypto failure: ${err.code}`);
}
throw err;
}
}
async function unwrapAesKey(
recipientPrivateKey: Uint8Array,
wrappedKey: Uint8Array
): Promise<Uint8Array> {
return await rsa.decrypt(recipientPrivateKey, wrappedKey);
}
// 4096-bit keypair for higher-security contexts
const { publicKey, privateKey } = await rsa.generateKeypair("RSA-OAEP", 4096);
```
## API Reference
### `rsa.generateKeypair(algorithm, modulusLength?): Promise<{ publicKey, privateKey }>`
Generates an RSA keypair.
| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `algorithm` | `RsaAlgorithm` | — | `"RSA-PSS"` \| `"RSA-OAEP"` \| `"RSASSA-PKCS1-v1_5"` |
| `modulusLength` | `number` | `2048` | Key bit length (2048 or 4096 recommended) |
**Returns:** `Promise<{ publicKey: Uint8Array; privateKey: Uint8Array }>` — SPKI/PKCS8 DER bytes.
**Throws:** `BrowserCryptoError` — `BROWSER_CRYPTO.KEY.GENERATION_FAILED`.
---
### `rsa.sign(algorithm, privateKey, message): Promise<Uint8Array>`
Signs a message. Use with `"RSA-PSS"` or `"RSASSA-PKCS1-v1_5"`.
| Parameter | Type | Description |
|-----------|------|-------------|
| `algorithm` | `RsaAlgorithm` | Must match keypair generation algorithm |
| `privateKey` | `Uint8Array` | PKCS8 DER private key |
| `message` | `Uint8Array` | Message to sign |
**Returns:** `Promise<Uint8Array>` — signature bytes.
**Throws:** `BrowserCryptoError` — `BROWSER_CRYPTO.OPERATION.FAILED`.
---
### `rsa.verify(algorithm, publicKey, signature, message): Promise<boolean>`
| Parameter | Type | Description |
|-----------|------|-------------|
| `algorithm` | `RsaAlgorithm` | Must match signing algorithm |
| `publicKey` | `Uint8Array` | SPKI DER public key |
| `signature` | `Uint8Array` | Signature bytes |
| `message` | `Uint8Array` | Original message |
**Returns:** `Promise<boolean>`.
---
### `rsa.encrypt(publicKey, plaintext): Promise<Uint8Array>`
RSA-OAEP encryption. For RSA-2048, plaintext must be ≤ 190 bytes (2048-bit modulus minus OAEP overhead).
---
### `rsa.decrypt(privateKey, ciphertext): Promise<Uint8Array>`
RSA-OAEP decryption.
---
### `type RsaAlgorithm`
`"RSA-PSS" | "RSA-OAEP" | "RSASSA-PKCS1-v1_5"`
## Implementation Notes
All RSA operations use SHA-256 as the hash algorithm, which is the most widely supported and appropriate choice for 2048- and 4096-bit RSA. RSA-PSS uses a 32-byte (256-bit) salt, matching the hash output length, which is the PKCS#1v2.2 recommendation.
The `encrypt()` and `decrypt()` methods are hardcoded to RSA-OAEP — only RSA-OAEP provides semantic security for asymmetric encryption. `RSA-PSS` and `RSASSA-PKCS1-v1_5` are signing-only algorithms and cannot be used with `encrypt`/`decrypt`.
Key generation for RSA-2048 typically takes 50–200 ms in `crypto.subtle`; RSA-4096 can take 500–2000 ms. This is significantly slower than elliptic-curve key generation (< 5 ms). All operations use `await` and do not block the main thread.

View File

@@ -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

View File

@@ -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.

View File

@@ -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.

View File

@@ -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`.

View File

@@ -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.

View File

@@ -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.

View File

@@ -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.

View File

@@ -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.

View File

@@ -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`.

View File

@@ -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.

View File

@@ -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.

View File

@@ -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.

View File

@@ -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.

View File

@@ -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.

View File

@@ -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.

View File

@@ -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.

View File

@@ -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).

View File

@@ -0,0 +1,96 @@
# streaming-aead
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`streaming-aead.ts` is the engine-agnostic streaming AEAD generator pair (`encryptStreamAead` / `decryptStreamAead`) in `@suaveplan/crypto-cipher`. It frames an arbitrary `AsyncIterable<Uint8Array>` source of plaintext into a self-describing wire format with per-chunk authentication and a final-flag terminator that binds the total chunk count into the AEAD tag, so silent truncation of the trailing chunk is impossible.
The generators delegate per-chunk crypto to a pluggable `StreamingAeadEngine` (default: `xchacha20StreamingEngine`). The engine boundary is the substitution point that lets callers swap in AES-256-GCM, XChaCha20-Poly1305, or a hardware-backed engine without changing the framing protocol or the public API surface.
## Features
- Async-generator API consumes any `AsyncIterable<Uint8Array>` or sync `Iterable<Uint8Array>` source.
- Framed wire format: 8-byte header prefix + variable-length base nonce, then `[ciphertextLen BE | finalFlag | ciphertext‖tag]` per chunk.
- Final-flag terminator chunk binds the total chunk count into the AEAD tag.
- `AbortSignal` propagation — aborting flushes the in-flight chunk and emits `STREAM_ABORTED`.
- Optional `onHeader` / `onChunk` callbacks for attestation and observability without breaking the streaming contract.
- Engine pluggability (the default is XChaCha20-Poly1305; AES-256-GCM is available via `aesGcmStreamingEngine`).
- Counter-overflow guard at `2^32` chunks — emits `STREAM_OUT_OF_ORDER`.
- Tag-mismatch detection on every chunk emits `STREAM_TAG_MISMATCH`.
- `allowUnterminated` opt-in for streams produced by upstream tooling that omit the final-flag.
## Usage: Basic Example
```ts
import {
encryptStreamAead,
decryptStreamAead,
} from "@suaveplan/crypto-cipher/streaming/streaming-aead";
async function* source() {
yield new TextEncoder().encode("part one ");
yield new TextEncoder().encode("part two");
}
const key = crypto.getRandomValues(new Uint8Array(32));
const wire: Uint8Array[] = [];
for await (const chunk of encryptStreamAead(key, source())) {
wire.push(chunk);
}
async function* replay() {
for (const c of wire) yield c;
}
const out: Uint8Array[] = [];
for await (const chunk of decryptStreamAead(key, replay())) {
out.push(chunk);
}
```
## Usage: Advanced Example
```ts
import {
encryptStreamAead,
decryptStreamAead,
} from "@suaveplan/crypto-cipher/streaming/streaming-aead";
import { aesGcmStreamingEngine } from "@suaveplan/crypto-cipher/streaming/streaming-aead-aes-gcm";
const key = crypto.getRandomValues(new Uint8Array(32));
const baseNonce = crypto.getRandomValues(new Uint8Array(8));
const ctrl = new AbortController();
setTimeout(() => ctrl.abort(), 250); // abort half-way through a long stream
try {
for await (const chunk of encryptStreamAead(key, longSource(), {
engine: aesGcmStreamingEngine,
baseNonce,
signal: ctrl.signal,
onHeader: (h) => console.log("header", h.engineId),
onChunk: (t) => console.log("chunk", t.counter, t.finalFlag),
})) {
sink.write(chunk);
}
} catch (err) {
if (err instanceof Error && (err as { code?: string }).code === "CRYPTO.CIPHER.STREAM_ABORTED") {
console.warn("aborted at chunk", (err as { context?: { chunksProcessed: number } }).context?.chunksProcessed);
}
}
```
## API Reference
- `encryptStreamAead(key, source, options?): AsyncGenerator<Uint8Array, void>` — frames `source` chunk-by-chunk and yields the wire bytes.
- `decryptStreamAead(key, source, options?): AsyncGenerator<Uint8Array, void>` — consumes wire bytes from `source`, yields recovered plaintext chunks.
- `EncryptStreamAeadOptions { engine?, baseNonce?, signal?, onHeader?, onChunk? }`.
- `DecryptStreamAeadOptions { engine?, allowUnterminated?, signal?, onHeader?, onChunk? }`.
Errors: every failure path throws `CryptoCipherError` with one of `STREAM_TAG_MISMATCH`, `STREAM_OUT_OF_ORDER`, `STREAM_ABORTED`, `STREAM_INVALID_NONCE_BASE`.
## Implementation Notes
The wire header is 8 bytes (magic[4] || engineWireId[1] || version[1] || baseNonceLen[2]) followed by the base nonce. Each chunk frame is `[ciphertextLen BE | finalFlag | ciphertext‖tag]`. The terminator frame uses an empty plaintext under finalFlag=1 so the AEAD tag binds the chunk count — a truncated stream that omits the terminator fails at decrypt time with `STREAM_TAG_MISMATCH`. The high bit of the 32-bit chunk counter carries the final-flag bit, so terminator and data chunks at the same counter index produce different nonces and cannot be confused.

View File

@@ -0,0 +1 @@
# @suaveplan/crypto-cli - Suaveplan keychain CLI — root + server identity lifecycle (init, roll, revoke, inspect, verify, export-public, restore)

View File

@@ -0,0 +1,41 @@
# @suaveplan/crypto Changelog
## 0.0.0 — suaveplan rebuild
Complete rewrite with engine-interface architecture.
### Architecture
- **Engine interfaces** — `AeadEngine`, `SignEngine`, `AgreeEngine`, `SealedBoxEngine`, `AuthBoxEngine` as pure TypeScript interface types. Noble defaults ship in-package; WebCrypto and libsodium engines live in `@suaveplan/crypto-browser` and `@suaveplan/crypto-sodium`.
- **`ByteSource` rename** — `Rng = (n: number) => Uint8Array` renamed to `ByteSource` to avoid collision with `@suaveplan/types`'s `Rng = { nextUint32(): number; nextFloat(): number }`. `Rng` kept as a deprecated alias.
- **`@suaveplan/crypto-hash` dependency** — hashing (SHA-2, HMAC, HKDF, PBKDF2, Argon2id) split into `@suaveplan/crypto-hash`. `@noble/hashes` is no longer a direct dependency of `@suaveplan/crypto`.
### New features
- `XChaCha20-Poly1305` added as a third AEAD algorithm (`AEAD_XCHACHA_NONCE_LENGTH = 24`)
- `sealedBox.seal` / `sealedBox.open` — anonymous-sender encryption (X25519 + XChaCha20-Poly1305 + HKDF-SHA256)
- `authBox.seal` / `authBox.open` — authenticated box (X25519 + XChaCha20-Poly1305 + HKDF-SHA256)
- `defaultByteSource` exported as canonical name alongside deprecated `defaultRng`
- All public functions accept trailing `{ engine?: <EngineType> }` for engine substitution
### Directory restructure
| Old path | New path |
|---|---|
| `src/random/random-bytes.ts` | `src/primitives/random-bytes.ts` |
| `src/compare/constant-time-equal.ts` | `src/primitives/constant-time-equal.ts` |
| `src/zeroize/zeroize.ts` | `src/primitives/zeroize.ts` |
| `src/hash/` | Removed → `@suaveplan/crypto-hash` |
| `src/hmac/` | Removed → `@suaveplan/crypto-hash` |
| `src/kdf/` | Removed → `@suaveplan/crypto-hash` |
### Subpath exports updated
New: `./primitives/random-bytes`, `./primitives/constant-time-equal`, `./primitives/zeroize`, `./sealed-box`, `./auth-box`, `./engines`
Removed: `./random`, `./compare`, `./hash`, `./hmac`, `./kdf`
### Quality
- 100% statement / branch / function / line coverage
- `tests/no-disallowed.test.ts` guards now point to `src/primitives/random-bytes.ts`
- `tests/dep-graph.test.ts` asserts `@noble/hashes` is NOT a direct dep

View File

@@ -0,0 +1,199 @@
# @suaveplan/crypto-core - Universal cryptographic primitives (formerly @suaveplan/crypto) — AEAD (AES-256-GCM, ChaCha20-Poly1305, XChaCha20-Poly1305), Ed25519 / X25519 / ECDSA-P256, sealed-box, auth-box, constant-time compare, zeroize. Engine-pluggable via noble defaults. Consumed by @suaveplan/crypto (the opinionated keychain/vault/packer layer); other packages must go through @suaveplan/crypto, not this package directly.
## Summary
Universal cryptographic primitives — engine-pluggable AEAD, Ed25519/ECDSA-P256 signing, X25519 key agreement, sealed-box, and auth-box. Built on audited [@noble/*](https://paulmillr.com/noble/) libraries with zero WASM.
<!-- begin:auto-docs -->
## Documentation tree
- [aead](./src/aead/aead.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->- noble-aead-engine — <!-- begin:auto-docs --> _(no su…
- [noble-aead-engine](./src/aead/noble-aead-engine/noble-aead-engine.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [noble-aead-engine](./src/aead/noble-aead-engine/noble-aead-engine.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [aead](./src/aead/aead.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->- noble-aead-engine — <!-- begin:auto-docs --> _(no su…
- [noble-agree-engine](./src/agree/noble-agree-engine/noble-agree-engine.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [noble-agree-engine](./src/agree/noble-agree-engine/noble-agree-engine.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [x25519](./src/agree/x25519/x25519.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [x25519](./src/agree/x25519/x25519.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [auth-box](./src/auth-box/auth-box.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->- noble-auth-box-engine — <!-- begin:auto-docs --> _(n…
- [noble-auth-box-engine](./src/auth-box/noble-auth-box-engine/noble-auth-box-engine.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [noble-auth-box-engine](./src/auth-box/noble-auth-box-engine/noble-auth-box-engine.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [auth-box](./src/auth-box/auth-box.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->- noble-auth-box-engine — <!-- begin:auto-docs --> _(n…
- [codes](./src/errors/codes/codes.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [codes](./src/errors/codes/codes.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [crypto-error](./src/errors/crypto-error/crypto-error.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [crypto-error](./src/errors/crypto-error/crypto-error.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [constant-time-equal](./src/primitives/constant-time-equal/constant-time-equal.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [constant-time-equal](./src/primitives/constant-time-equal/constant-time-equal.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [random-bytes](./src/primitives/random-bytes/random-bytes.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [random-bytes](./src/primitives/random-bytes/random-bytes.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [zeroize](./src/primitives/zeroize/zeroize.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [zeroize](./src/primitives/zeroize/zeroize.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [sealed-box](./src/sealed-box/sealed-box.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->- noble-sealed-box-engine — <!-- begin:auto-docs --> _…
- [noble-sealed-box-engine](./src/sealed-box/noble-sealed-box-engine/noble-sealed-box-engine.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [noble-sealed-box-engine](./src/sealed-box/noble-sealed-box-engine/noble-sealed-box-engine.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [sealed-box](./src/sealed-box/sealed-box.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->- noble-sealed-box-engine — <!-- begin:auto-docs --> _…
- [ecdsa-p256](./src/sign/ecdsa-p256/ecdsa-p256.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [ecdsa-p256](./src/sign/ecdsa-p256/ecdsa-p256.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [ed25519](./src/sign/ed25519/ed25519.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [ed25519](./src/sign/ed25519/ed25519.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [noble-sign-engine](./src/sign/noble-sign-engine/noble-sign-engine.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [noble-sign-engine](./src/sign/noble-sign-engine/noble-sign-engine.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
<!-- end:auto-docs --> - [noble-aead-engine](./src/aead/noble-aead-engine/noble-aead-engine.md) — <!-- begin:auto-docs --> _(no submodules)_- [noble-aead-engine](./src/aead/noble-aead-engine/noble-aead-engine.md) — <!-- begin:auto-docs --> _(no submodules)_- [aead](./src/aead/aead.md) — <!-- begin:auto-docs --> _(no submodules)_- [noble-agree-engine](./src/agree/noble-agree-engine/noble-agree-engine.md) — <!-- begin:auto-docs --> _(no submodules)_- [noble-agree-engine](./src/agree/noble-agree-engine/noble-agree-engine.md) — <!-- begin:auto-docs --> _(no submodules)_- [x25519](./src/agree/x25519/x25519.md) — <!-- begin:auto-docs --> _(no submodules)_- [x25519](./src/agree/x25519/x25519.md) — <!-- begin:auto-docs --> _(no submodules)_- [auth-box](./src/auth-box/auth-box.md) — <!-- begin:auto-docs --> _(no submodules)_- [noble-auth-box-engine](./src/auth-box/noble-auth-box-engine/noble-auth-box-engine.md) — <!-- begin:auto-docs --> _(no submodules)_- [noble-auth-box-engine](./src/auth-box/noble-auth-box-engine/noble-auth-box-engine.md) — <!-- begin:auto-docs --> _(no submodules)_- [auth-box](./src/auth-box/auth-box.md) — <!-- begin:auto-docs --> _(no submodules)_- [codes](./src/errors/codes/codes.md) — <!-- begin:auto-docs --> _(no submodules)_- [codes](./src/errors/codes/codes.md) — <!-- begin:auto-docs --> _(no submodules)_- [crypto-error](./src/errors/crypto-error/crypto-error.md) — <!-- begin:auto-docs --> _(no submodules)_- [crypto-error](./src/errors/crypto-error/crypto-error.md) — <!-- begin:auto-docs --> _(no submodules)_- [constant-time-equal](./src/primitives/constant-time-equal/constant-time-equal.md) — <!-- begin:auto-docs --> _(no submodules)_- [constant-time-equal](./src/primitives/constant-time-equal/constant-time-equal.md) — <!-- begin:auto-docs --> _(no submodules)_- [random-bytes](./src/primitives/random-bytes/random-bytes.md) — <!-- begin:auto-docs --> _(no submodules)_- [random-bytes](./src/primitives/random-bytes/random-bytes.md) — <!-- begin:auto-docs --> _(no submodules)_- [zeroize](./src/primitives/zeroize/zeroize.md) — <!-- begin:auto-docs --> _(no submodules)_- [zeroize](./src/primitives/zeroize/zeroize.md) — <!-- begin:auto-docs --> _(no submodules)_- [sealed-box](./src/sealed-box/sealed-box.md) — <!-- begin:auto-docs --> _(no submodules)_- [noble-sealed-box-engine](./src/sealed-box/noble-sealed-box-engine/noble-sealed-box-engine.md) — <!-- begin:auto-docs --> _(no submodules)_- [noble-sealed-box-engine](./src/sealed-box/noble-sealed-box-engine/noble-sealed-box-engine.md) — <!-- begin:auto-docs --> _(no submodules)_- [sealed-box](./src/sealed-box/sealed-box.md) — <!-- begin:auto-docs --> _(no submodules)_- [ecdsa-p256](./src/sign/ecdsa-p256/ecdsa-p256.md) — <!-- begin:auto-docs --> _(no submodules)_- [ecdsa-p256](./src/sign/ecdsa-p256/ecdsa-p256.md) — <!-- begin:auto-docs --> _(no submodules)_- [ed25519](./src/sign/ed25519/ed25519.md) — <!-- begin:auto-docs --> _(no submodules)_- [ed25519](./src/sign/ed25519/ed25519.md) — <!-- begin:auto-docs --> _(no submodules)_- [noble-sign-engine](./src/sign/noble-sign-engine/noble-sign-engine.md) — <!-- begin:auto-docs --> _(no submodules)_- [noble-sign-engine](./src/sign/noble-sign-engine/noble-sign-engine.md) — <!-- begin:auto-docs --> _(no submodules)_- noble-aead-engine — <!-- beg…
- [noble-aead-engine](./src/aead/noble-aead-engine/noble-aead-engine.md) — <!-- begin:auto-docs --> _(no submodules)_- [noble-aead-engine](./src/aead/noble-aead-engine/noble-aead-engine.md) — <!-- begin:auto-docs --> _(no submodules)_- [aead](./src/aead/aead.md) — <!-- begin:auto-docs --> _(no submodules)_- noble-aead-engine — <!-- beg…
- [noble-agree-engine](./src/agree/noble-agree-engine/noble-agree-engine.md) — <!-- begin:auto-docs --> _(no submodules)_- [noble-agree-engine](./src/agree/noble-agree-engine/noble-agree-engine.md) — <!-- begin:auto-docs --> _(no submodules)_- [x25519](./src/agree/x25519/x25519.md) — <!-- begin:auto-docs --> _(no submodules)_- [x25519](./src/agree/x25519/x25519.md) — <!-- begin:auto-docs --> _(no submodules)_- [auth-box](./src/auth-box/auth-box.md) — <!-- begin:auto-docs --> _(no submodules)_- noble-auth-box-engine — <!--…
- [noble-auth-box-engine](./src/auth-box/noble-auth-box-engine/noble-auth-box-engine.md) — <!-- begin:auto-docs --> _(no submodules)_- [noble-auth-box-engine](./src/auth-box/noble-auth-box-engine/noble-auth-box-engine.md) — <!-- begin:auto-docs --> _(no submodules)_- [auth-box](./src/auth-box/auth-box.md) — <!-- begin:auto-docs --> _(no submodules)_- noble-auth-box-engine — <!--…
- [codes](./src/errors/codes/codes.md) — <!-- begin:auto-docs --> _(no submodules)_- [codes](./src/errors/codes/codes.md) — <!-- begin:auto-docs --> _(no submodules)_- [crypto-error](./src/errors/crypto-error/crypto-error.md) — <!-- begin:auto-docs --> _(no submodules)_- [crypto-error](./src/errors/crypto-error/crypto-error.md) — <!-- begin:auto-docs --> _(no submodules)_- [constant-time-equal](./src/primitives/constant-time-equal/constant-time-equal.md) — <!-- begin:auto-docs --> _(no submodules)_- [constant-time-equal](./src/primitives/constant-time-equal/constant-time-equal.md) — <!-- begin:auto-docs --> _(no submodules)_- [random-bytes](./src/primitives/random-bytes/random-bytes.md) — <!-- begin:auto-docs --> _(no submodules)_- [random-bytes](./src/primitives/random-bytes/random-bytes.md) — <!-- begin:auto-docs --> _(no submodules)_- [zeroize](./src/primitives/zeroize/zeroize.md) — <!-- begin:auto-docs --> _(no submodules)_- [zeroize](./src/primitives/zeroize/zeroize.md) — <!-- begin:auto-docs --> _(no submodules)_- [sealed-box](./src/sealed-box/sealed-box.md) — <!-- begin:auto-docs --> _(no submodules)_- noble-sealed-box-engine — <!…
- [noble-sealed-box-engine](./src/sealed-box/noble-sealed-box-engine/noble-sealed-box-engine.md) — <!-- begin:auto-docs --> _(no submodules)_- [noble-sealed-box-engine](./src/sealed-box/noble-sealed-box-engine/noble-sealed-box-engine.md) — <!-- begin:auto-docs --> _(no submodules)_- [sealed-box](./src/sealed-box/sealed-box.md) — <!-- begin:auto-docs --> _(no submodules)_- noble-sealed-box-engine — <!…
- [ecdsa-p256](./src/sign/ecdsa-p256/ecdsa-p256.md) — <!-- begin:auto-docs --> _(no submodules)_- [ecdsa-p256](./src/sign/ecdsa-p256/ecdsa-p256.md) — <!-- begin:auto-docs --> _(no submodules)_- [ed25519](./src/sign/ed25519/ed25519.md) — <!-- begin:auto-docs --> _(no submodules)_- [ed25519](./src/sign/ed25519/ed25519.md) — <!-- begin:auto-docs --> _(no submodules)_- [noble-sign-engine](./src/sign/noble-sign-engine/noble-sign-engine.md) — <!-- begin:auto-docs --> _(no submodules)_- [noble-sign-engine](./src/sign/noble-sign-engine/noble-sign-engine.md) — <!-- begin:auto-docs --> _(no submodules)_- noble-aead-engine — <!-- begin:auto-docs --> _(no s…
- [noble-aead-engine](./src/aead/noble-aead-engine/noble-aead-engine.md) — <!-- begin:auto-docs --> _(no submodules)_- [noble-aead-engine](./src/aead/noble-aead-engine/noble-aead-engine.md) — <!-- begin:auto-docs --> _(no submodules)_- [aead](./src/aead/aead.md) — <!-- begin:auto-docs --> _(no submodules)_- noble-aead-engine — <!-- begin:auto-docs --> _(no s…
- [noble-agree-engine](./src/agree/noble-agree-engine/noble-agree-engine.md) — <!-- begin:auto-docs --> _(no submodules)_- [noble-agree-engine](./src/agree/noble-agree-engine/noble-agree-engine.md) — <!-- begin:auto-docs --> _(no submodules)_- [x25519](./src/agree/x25519/x25519.md) — <!-- begin:auto-docs --> _(no submodules)_- [x25519](./src/agree/x25519/x25519.md) — <!-- begin:auto-docs --> _(no submodules)_- [auth-box](./src/auth-box/auth-box.md) — <!-- begin:auto-docs --> _(no submodules)_- noble-auth-box-engine — <!-- begin:auto-docs --> _(…
- [noble-auth-box-engine](./src/auth-box/noble-auth-box-engine/noble-auth-box-engine.md) — <!-- begin:auto-docs --> _(no submodules)_- [noble-auth-box-engine](./src/auth-box/noble-auth-box-engine/noble-auth-box-engine.md) — <!-- begin:auto-docs --> _(no submodules)_- [auth-box](./src/auth-box/auth-box.md) — <!-- begin:auto-docs --> _(no submodules)_- noble-auth-box-engine — <!-- begin:auto-docs --> _(…
- [codes](./src/errors/codes/codes.md) — <!-- begin:auto-docs --> _(no submodules)_- [codes](./src/errors/codes/codes.md) — <!-- begin:auto-docs --> _(no submodules)_- [crypto-error](./src/errors/crypto-error/crypto-error.md) — <!-- begin:auto-docs --> _(no submodules)_- [crypto-error](./src/errors/crypto-error/crypto-error.md) — <!-- begin:auto-docs --> _(no submodules)_- [constant-time-equal](./src/primitives/constant-time-equal/constant-time-equal.md) — <!-- begin:auto-docs --> _(no submodules)_- [constant-time-equal](./src/primitives/constant-time-equal/constant-time-equal.md) — <!-- begin:auto-docs --> _(no submodules)_- [random-bytes](./src/primitives/random-bytes/random-bytes.md) — <!-- begin:auto-docs --> _(no submodules)_- [random-bytes](./src/primitives/random-bytes/random-bytes.md) — <!-- begin:auto-docs --> _(no submodules)_- [zeroize](./src/primitives/zeroize/zeroize.md) — <!-- begin:auto-docs --> _(no submodules)_- [zeroize](./src/primitives/zeroize/zeroize.md) — <!-- begin:auto-docs --> _(no submodules)_- [sealed-box](./src/sealed-box/sealed-box.md) — <!-- begin:auto-docs --> _(no submodules)_- noble-sealed-box-engine — <!-- begin:auto-docs -->…
- [noble-sealed-box-engine](./src/sealed-box/noble-sealed-box-engine/noble-sealed-box-engine.md) — <!-- begin:auto-docs --> _(no submodules)_- [noble-sealed-box-engine](./src/sealed-box/noble-sealed-box-engine/noble-sealed-box-engine.md) — <!-- begin:auto-docs --> _(no submodules)_- [sealed-box](./src/sealed-box/sealed-box.md) — <!-- begin:auto-docs --> _(no submodules)_- noble-sealed-box-engine — <!-- begin:auto-docs -->…
- [ecdsa-p256](./src/sign/ecdsa-p256/ecdsa-p256.md) — <!-- begin:auto-docs --> _(no submodules)_- [ecdsa-p256](./src/sign/ecdsa-p256/ecdsa-p256.md) — <!-- begin:auto-docs --> _(no submodules)_- [ed25519](./src/sign/ed25519/ed25519.md) — <!-- begin:auto-docs --> _(no submodules)_- [ed25519](./src/sign/ed25519/ed25519.md) — <!-- begin:auto-docs --> _(no submodules)_- [noble-sign-engine](./src/sign/noble-sign-engine/noble-sign-engine.md) — <!-- begin:auto-docs --> _(no submodules)_- [noble-sign-engine](./src/sign/noble-sign-engine/noble-sign-engine.md) — <!-- begin:auto-docs --> _(no submodules)_- [noble-aead-engine](./src/aead/noble-aead-engine/noble-aead-engine.md) — <!-- begin:auto-docs --> _(no submodules)_ - [noble-aead-engine](./src/aead/noble-aead-engine/noble-aead-engine.md) — <!-- begin:auto-docs --> _(no submodules)_- [aead](./src/aead/aead.md) — <!-- begin:auto-docs --> _(no submodules)_- [noble-agree-engine](./src/agree/noble-agree-engine/noble-agree-engine.md) — <!-- begin:auto-docs --> _(no submodules)_ - [noble-agree-engine](./src/agree/noble-agree-engine/noble-agree-engine.md) — <!-- begin:auto-docs --> _(no submodules)_- [x25519](./src/agree/x25519/x25519.md) — <!-- begin:auto-docs --> _(no submodules)_ - [x25519](./src/agree/x25519/x25519.md) — <!-- begin:auto-docs --> _(no submodules)_- [auth-box](./src/auth-box/auth-box.md) — <!-- begin:auto-docs --> _(no submodules)_ - [noble-auth-box-engine](./src/auth-box/noble-auth-box-engine/noble-auth-box-engine.md) — <!-- begin:auto-docs --> _(no submodules)_ - [noble-auth-box-engine](./src/auth-box/noble-auth-box-engine/noble-auth-box-engine.md) — <!-- begin:auto-docs --> _(no submodules)_ - [auth-box](./src/auth-box/auth-box.md) — <!-- begin:auto-docs --> _(no submodules)_- [codes](./src/errors/codes/codes.md) — <!-- begin:auto-docs --> _(no submodules)_ - [codes](./src/errors/codes/codes.md) — <!-- begin:auto-docs --> _(no submodules)_- [crypto-error](./src/errors/crypto-error/crypto-error.md) — <!-- begin:auto-docs --> _(no submodules)_ - [crypto-error](./src/errors/crypto-error/crypto-error.md) — <!-- begin:auto-docs --> _(no submodules)_- [constant-time-equal](./src/primitives/constant-time-equal/constant-time-equal.md) — <!-- begin:auto-docs --> _(no submodules)_ - [constant-time-equal](./src/primitives/constant-time-equal/constant-time-equal.md) — <!-- begin:auto-docs --> _(no submodules)_- [random-bytes](./src/primitives/random-bytes/random-bytes.md) — <!-- begin:auto-docs --> _(no submodules)_ - [random-bytes](./src/primitives/random-bytes/random-bytes.md) — <!-- begin:auto-docs --> _(no submodules)_- [zeroize](./src/primitives/zeroize/zeroize.md) — <!-- begin:auto-docs --> _(no submodules)_ - [zeroize](./src/primitives/zeroize/zeroize.md) — <!-- begin:auto-docs --> _(no submodules)_- [sealed-box](./src/sealed-box/sealed-box.md) — <!-- begin:auto-docs --> _(no submodules)_ - [noble-sealed-box-engine](./src/sealed-box/noble-sealed-box-engine/noble-sealed-box-engine.md) — <!-- begin:auto-docs --> _(no submodules)_ - [noble-sealed-box-engine](./src/sealed-box/noble-sealed-box-engine/noble-sealed-box-engine.md) — <!-- begin:auto-docs --> _(no submodules)_ - [sealed-box](./src/sealed-box/sealed-box.md) — <!-- begin:auto-docs --> _(no submodules)_- [ecdsa-p256](./src/sign/ecdsa-p256/ecdsa-p256.md) — <!-- begin:auto-docs --> _(no submodules)_ - [ecdsa-p256](./src/sign/ecdsa-p256/ecdsa-p256.md) — <!-- begin:auto-docs --> _(no submodules)_- [ed25519](./src/sign/ed25519/ed25519.md) — <!-- begin:auto-docs --> _(no submodules)_ - [ed25519](./src/sign/ed25519/ed25519.md) — <!-- begin:auto-docs --> _(no submodules)_- [noble-sign-engine](./src/sign/noble-sign-engine/noble-sign-engine.md) — <!-- begin:auto-docs --> _(no submodules)_ - [noble-sign-engine](./src/sign/noble-sign-engine/noble-sign-engine.md) — <!-- begin:auto-docs --> _(no submodules)_
## Install
```bash
bun add @suaveplan/crypto
```
## Primitives
| Surface | What |
|---|---|
| `randomBytes(n, { byteSource? })` | CSPRNG bytes (WebCrypto default; injectable for tests) |
| `constantTimeEqual(a, b)` | Constant-time byte comparison |
| `zeroize(buf)` | Fill a `Uint8Array` with zeros |
| `aeadEncrypt` / `aeadDecrypt` | AES-256-GCM, ChaCha20-Poly1305, XChaCha20-Poly1305 |
| `ed25519.{generateKeypair, sign, verify}` | Ed25519 signing |
| `ecdsaP256.{generateKeypair, sign, verify}` | ECDSA-P256 signing |
| `x25519.{generateKeypair, agree}` | X25519 ECDH key agreement |
| `sealedBox.{seal, open}` | Anonymous-sender sealed box (X25519 + XChaCha20-Poly1305) |
| `authBox.{seal, open}` | Authenticated box (X25519 + XChaCha20-Poly1305) |
> Hashing (SHA-2, HMAC, HKDF, PBKDF2, Argon2id) lives in [`@suaveplan/crypto-hash`](../hash).
## Engine interfaces
Every operation accepts an optional `{ engine?: <EngineType> }` argument. The default engines use `@noble/ciphers` and `@noble/curves`. Swap in a WebCrypto-backed or libsodium-backed engine without changing call sites.
```ts
import type { AeadEngine } from "@suaveplan/crypto-core/engines";
const webCryptoEngine: AeadEngine = { /* ... */ };
aeadEncrypt("AES-256-GCM", key, plaintext, { nonce }, { engine: webCryptoEngine });
```
## ByteSource
`ByteSource = (n: number) => Uint8Array` — the injected randomness type used for key generation. Replaces the old `Rng` alias (which conflicted with `@suaveplan/types`'s `Rng`).
```ts
import { randomBytes, type ByteSource } from "@suaveplan/crypto-core";
// Production
const key = randomBytes(32);
// Tests — deterministic
const seeded: ByteSource = (n) => new Uint8Array(n).fill(0x42);
const kp = ed25519.generateKeypair({ byteSource: seeded });
```
## AEAD
```ts
import { aeadEncrypt, aeadDecrypt, randomBytes } from "@suaveplan/crypto-core";
const key = randomBytes(32);
const nonce = randomBytes(12); // 12 for AES-GCM/ChaCha20, 24 for XChaCha20
const plaintext = new TextEncoder().encode("hello");
const ct = aeadEncrypt("AES-256-GCM", key, plaintext, { nonce });
const pt = aeadDecrypt("AES-256-GCM", key, ct, { nonce });
```
## Signing
```ts
import { ed25519, ecdsaP256 } from "@suaveplan/crypto-core";
const kp = ed25519.generateKeypair();
const sig = ed25519.sign(kp.secretKey, message);
ed25519.verify(kp.publicKey, sig, message); // true
```
## Key Agreement + Sealed Box
```ts
import { x25519, sealedBox, authBox, randomBytes } from "@suaveplan/crypto-core";
const alice = x25519.generateKeypair();
const bob = x25519.generateKeypair();
// Anonymous sender (sealed box)
const sealed = sealedBox.seal(bob.publicKey, plaintext);
const opened = sealedBox.open(bob.secretKey, bob.publicKey, sealed);
// Authenticated (auth box)
const nonce = randomBytes(24);
const ct = authBox.seal(alice.secretKey, bob.publicKey, plaintext, nonce);
const pt = authBox.open(alice.publicKey, bob.secretKey, ct, nonce);
```
## Error surface
Every failure surfaces as `CryptoError` (extends `SuaveplanError`) with a stable `CRYPTO.*` code. Error context never includes raw key bytes, plaintext, or authentication tags.
| Code | When |
|---|---|
| `CRYPTO.KEY.INVALID_LENGTH` | Key is not 32 bytes |
| `CRYPTO.AEAD.INVALID_NONCE` | Nonce length wrong for algorithm |
| `CRYPTO.AEAD.DECRYPT_FAILED` | Authentication tag mismatch |
| `CRYPTO.SIGN.INVALID_KEY` | Key is malformed |
| `CRYPTO.SIGN.VERIFY_FAILED` | Signature verification failed |
| `CRYPTO.RNG.INVALID_LENGTH` | `randomBytes(n)` called with negative/non-integer n |
| `CRYPTO.RNG.UNAVAILABLE` | No CSPRNG available |
## Subpath exports
```ts
import { randomBytes } from "@suaveplan/crypto-core/primitives/random-bytes";
import { constantTimeEqual } from "@suaveplan/crypto-core/primitives/constant-time-equal";
import { zeroize } from "@suaveplan/crypto-core/primitives/zeroize";
import { aeadEncrypt, aeadDecrypt } from "@suaveplan/crypto-core/aead";
import { ed25519, ecdsaP256 } from "@suaveplan/crypto-core/sign";
import { x25519 } from "@suaveplan/crypto-core/agree";
import { sealedBox } from "@suaveplan/crypto-core/sealed-box";
import { authBox } from "@suaveplan/crypto-core/auth-box";
import type { AeadEngine, SignEngine } from "@suaveplan/crypto-core/engines";
import { CryptoError, CRYPTO_CODES } from "@suaveplan/crypto-core/errors";
```
## Co-located docs
- [`src/primitives/random-bytes.md`](src/primitives/random-bytes.md)
- [`src/primitives/constant-time-equal.md`](src/primitives/constant-time-equal.md)
- [`src/primitives/zeroize.md`](src/primitives/zeroize.md)
- [`src/aead/aead.md`](src/aead/aead.md)
- [`src/sign/ed25519.md`](src/sign/ed25519.md)
- [`src/sign/ecdsa-p256.md`](src/sign/ecdsa-p256.md)
- [`src/agree/x25519.md`](src/agree/x25519.md)
- [`src/sealed-box/sealed-box.md`](src/sealed-box/sealed-box.md)
- [`src/auth-box/auth-box.md`](src/auth-box/auth-box.md)
## Foundation rules
- Runtime deps: `@noble/ciphers`, `@noble/curves`, `@suaveplan/crypto-hash`, `@suaveplan/error`, `@suaveplan/types`, `tslib`, `zod`
- `sideEffects: false`, per-primitive subpath exports
- Zero `Math.random`, `Date.now`, or `crypto.getRandomValues` outside `src/primitives/random-bytes.ts` (guarded by `tests/no-disallowed.test.ts`)
- 100% statement / branch / function / line coverage (Layer 1/2 foundation rule)
- fast-check property tests for every primitive
## Testing
```bash
bunx turbo lint typecheck test:coverage --filter=@suaveplan/crypto
```

View File

@@ -0,0 +1,125 @@
# aead
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
- [noble-aead-engine](./noble-aead-engine/noble-aead-engine.md) — <!-- begin:auto-docs --> _(no submodules)_- [aead](./aead.md) — <!-- begin:auto-docs --> _(no submodules)_- noble-aead-engine — <!-- begin:auto-docs --> _(no su…
- [noble-aead-engine](./noble-aead-engine/noble-aead-engine.md) — <!-- begin:auto-docs --> _(no submodules)_- [aead](./aead.md) — <!-- begin:auto-docs --> _(no submodules)_- [noble-aead-engine](./noble-aead-engine/noble-aead-engine.md) — <!-- begin:auto-docs --> _(no submodules)_- [aead](./aead.md) — <!-- begin:auto-docs --> _(no submodules)_- noble-aead-engine — <!-- beg…
- [noble-aead-engine](./noble-aead-engine/noble-aead-engine.md) — <!-- begin:auto-docs --> _(no submodules)_- [aead](./aead.md) — <!-- begin:auto-docs --> _(no submodules)_- noble-aead-engine — <!-- begin:auto-docs --> _(no s…
- [noble-aead-engine](./noble-aead-engine/noble-aead-engine.md) — <!-- begin:auto-docs --> _(no submodules)_- [aead](./aead.md) — <!-- begin:auto-docs --> _(no submodules)_
## Purpose
Provides authenticated encryption with associated data (AEAD) facade functions backed by `@noble/ciphers`. AEAD combines confidentiality and integrity in a single operation — encrypted data includes an authentication tag that is verified before decryption, making it safe against ciphertext tampering. The facade is engine-pluggable: pass `{ engine }` to swap in a WebCrypto-backed or libsodium-backed implementation without changing calling code.
## Features
- Three algorithms: `AES-256-GCM`, `ChaCha20-Poly1305`, and `XChaCha20-Poly1305`
- Engine-pluggable via `AeadEngine` interface (noble default, WebCrypto, libsodium)
- Key validation: rejects non-32-byte keys with `CRYPTO.KEY.INVALID_LENGTH`
- Nonce validation: 12 bytes for AES-GCM and ChaCha20, 24 bytes for XChaCha20
- Decrypt never leaks plaintext in error context
- Optional AAD (Additional Authenticated Data) for binding ciphertext to context
## Usage: Basic Example
```ts
import { aeadEncrypt, aeadDecrypt, randomBytes } from "@suaveplan/crypto-core";
const key = randomBytes(32);
const nonce = randomBytes(12);
const plaintext = new TextEncoder().encode("hello world");
const ciphertext = aeadEncrypt("AES-256-GCM", key, plaintext, { nonce });
const recovered = aeadDecrypt("AES-256-GCM", key, ciphertext, { nonce });
```
## Usage: Advanced Example
```ts
import { aeadEncrypt, aeadDecrypt, randomBytes } from "@suaveplan/crypto-core";
import type { AeadEngine } from "@suaveplan/crypto-core/engines";
// Custom engine stub for testing
const stubEngine: AeadEngine = {
encrypt: (_key, pt) => pt, // identity pass-through (test only!)
decrypt: (_key, ct) => ct,
};
// XChaCha20-Poly1305 with 24-byte nonce and AAD
const key = randomBytes(32);
const nonce = randomBytes(24);
const aad = new TextEncoder().encode("request-id:abc123");
const plaintext = new Uint8Array([1, 2, 3]);
const ct = aeadEncrypt("XChaCha20-Poly1305", key, plaintext, { nonce, aad });
const pt = aeadDecrypt("XChaCha20-Poly1305", key, ct, { nonce, aad });
```
## API Reference
### `AeadAlgorithm`
```ts
type AeadAlgorithm = "AES-256-GCM" | "ChaCha20-Poly1305" | "XChaCha20-Poly1305"
```
### `AEAD_KEY_LENGTH`
```ts
const AEAD_KEY_LENGTH = 32
```
Required key length in bytes for all algorithms.
### `AEAD_NONCE_LENGTH`
```ts
const AEAD_NONCE_LENGTH = 12
```
Required nonce length for AES-256-GCM and ChaCha20-Poly1305.
### `AEAD_XCHACHA_NONCE_LENGTH`
```ts
const AEAD_XCHACHA_NONCE_LENGTH = 24
```
Required nonce length for XChaCha20-Poly1305.
### `aeadEncrypt(algo, key, plaintext, options, opts?)`
```ts
function aeadEncrypt(
algo: AeadAlgorithm,
key: Uint8Array,
plaintext: Uint8Array,
options: AeadEncryptOptions,
opts?: AeadEngineOptions
): Uint8Array
```
Encrypts and authenticates `plaintext`. Returns `ciphertext || tag` (16-byte tag appended).
**Throws:** `CryptoError("CRYPTO.KEY.INVALID_LENGTH")` — key is not 32 bytes.
**Throws:** `CryptoError("CRYPTO.AEAD.INVALID_NONCE")` — nonce has wrong length for the algorithm.
### `aeadDecrypt(algo, key, ciphertext, options, opts?)`
```ts
function aeadDecrypt(
algo: AeadAlgorithm,
key: Uint8Array,
ciphertext: Uint8Array,
options: AeadDecryptOptions,
opts?: AeadEngineOptions
): Uint8Array
```
Verifies authentication tag and decrypts.
**Throws:** `CryptoError("CRYPTO.AEAD.DECRYPT_FAILED")` — tag mismatch or corrupted ciphertext. Error context never includes key or plaintext bytes.
## Implementation Notes
The noble library AES-256-GCM uses the native Web Crypto API when available for hardware acceleration. ChaCha20-Poly1305 and XChaCha20-Poly1305 are implemented in pure JavaScript, making them the better choice on platforms without AES hardware support (e.g., older ARM). XChaCha20-Poly1305 accepts a 24-byte nonce, which is large enough to generate randomly without birthday-bound concerns for up to 2^96 messages with the same key — whereas the 12-byte nonce of standard ChaCha20-Poly1305 requires a counter or careful uniqueness management.

View File

@@ -0,0 +1,103 @@
# NobleAeadEngine
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`NobleAeadEngine` is the default AEAD (Authenticated Encryption with Associated Data) engine for `@suaveplan/crypto`. It delegates to `@suaveplan/crypto-cipher` — itself backed by `@noble/ciphers` — so the build-time primitive selector defined in `crypto.config.ts` applies uniformly to all cipher calls, whether from the high-level facade or from this engine directly. The engine supports three algorithms: AES-256-GCM, ChaCha20-Poly1305, and XChaCha20-Poly1305, selected per instance at construction time.
## Features
- Implements the `AeadEngine` interface, making it pluggable into `aeadEncrypt` / `aeadDecrypt`
- Supports `AES-256-GCM`, `ChaCha20-Poly1305`, and `XChaCha20-Poly1305` via `AeadAlgorithm` discriminant
- Decryption failures are caught and re-thrown as `CryptoError` with code `AEAD.DECRYPT_FAILED` and structured context (algorithm name, ciphertext byte length)
- Algorithm selection delegates through `@suaveplan/crypto-cipher` subpaths, keeping the build-time selector effective
- Nonce is caller-supplied; the engine never generates nonces internally
## Usage: Basic Example
```typescript
import { NobleAeadEngine } from "@suaveplan/crypto-core/aead/noble-aead-engine";
import { randomBytes } from "@suaveplan/crypto-core";
const engine = new NobleAeadEngine("XChaCha20-Poly1305");
const key = randomBytes(32);
const nonce = randomBytes(24); // 24 bytes for XChaCha20
const plaintext = new TextEncoder().encode("hello world");
const ciphertext = engine.encrypt(key, plaintext, nonce);
const recovered = engine.decrypt(key, ciphertext, nonce);
console.log(new TextDecoder().decode(recovered)); // "hello world"
```
## Usage: Advanced Example
```typescript
import { NobleAeadEngine } from "@suaveplan/crypto-core/aead/noble-aead-engine";
import { CryptoError } from "@suaveplan/crypto-core/errors/crypto-error";
import { CRYPTO_CODES } from "@suaveplan/crypto-core/errors/codes";
import { randomBytes } from "@suaveplan/crypto-core";
const engine = new NobleAeadEngine("AES-256-GCM");
const key = randomBytes(32);
const nonce = randomBytes(12); // 12 bytes for AES-256-GCM
// Encrypt with additional authenticated data (AAD)
const plaintext = new TextEncoder().encode("sensitive payload");
const aad = new TextEncoder().encode("request-id:abc123");
const ciphertext = engine.encrypt(key, plaintext, nonce, aad);
// Verify AAD is checked during decrypt
try {
const wrongAad = new TextEncoder().encode("request-id:wrong");
engine.decrypt(key, ciphertext, nonce, wrongAad);
} catch (err) {
if (err instanceof CryptoError && err.code === CRYPTO_CODES["AEAD.DECRYPT_FAILED"]) {
console.log("AAD mismatch detected — message rejected");
}
}
// Correct AAD succeeds
const recovered = engine.decrypt(key, ciphertext, nonce, aad);
console.log(new TextDecoder().decode(recovered)); // "sensitive payload"
```
## API Reference
### `NobleAeadEngine`
```typescript
class NobleAeadEngine implements AeadEngine
```
**Constructor**
| Parameter | Type | Description |
|-----------|-----------------|-------------------------------------------------|
| `algo` | `AeadAlgorithm` | Selects the cipher: `"AES-256-GCM"`, `"ChaCha20-Poly1305"`, or `"XChaCha20-Poly1305"`. |
**`encrypt(key, plaintext, nonce, aad?): Uint8Array`**
| Parameter | Type | Description |
|-------------|----------------|------------------------------------------------------|
| `key` | `Uint8Array` | Symmetric key (32 bytes for all supported algorithms). |
| `plaintext` | `Uint8Array` | Message bytes to encrypt. |
| `nonce` | `Uint8Array` | Unique nonce (12 bytes for AES-GCM; 12 or 24 bytes for ChaCha variants). |
| `aad` | `Uint8Array \| undefined` | Optional additional authenticated data. |
Returns the ciphertext with authentication tag appended (16-byte tag for all three algorithms).
**`decrypt(key, ciphertext, nonce, aad?): Uint8Array`**
Same parameters as `encrypt`, with `ciphertext` in place of `plaintext`.
Throws `CryptoError` with code `CRYPTO_CODES["AEAD.DECRYPT_FAILED"]` if authentication verification fails or if the underlying primitive rejects the ciphertext for any reason. The error's `context` field includes `algorithm` and `ciphertextLength`.
## Implementation Notes
The internal `primitiveFor` function maps the `AeadAlgorithm` discriminant to the matching `@suaveplan/crypto-cipher` subpath import (`aes-256-gcm`, `xchacha20-poly1305`, or `chacha20-poly1305`). This avoids a dynamic dispatch switch on every call while still keeping each cipher behind its own subpath so tree-shaking is effective.
Encryption is allowed to propagate raw errors from the primitive (malformed key length, nonce length mismatch) because these represent programming errors — the caller supplied invalid arguments — rather than runtime failures from untrusted input. Decryption is wrapped in a try/catch because failures there arise from untrusted ciphertext and must be surfaced as a stable, typed `CryptoError` rather than a raw `Error` with an implementation-detail message from `@noble/ciphers`.

View File

@@ -0,0 +1,89 @@
# NobleX25519Engine
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`NobleX25519Engine` is the default key-agreement engine for `@suaveplan/crypto`. It implements the `AgreeEngine` interface using X25519 Diffie-Hellman as exposed through `@suaveplan/crypto-curve`, which in turn delegates to `@noble/curves`. The engine handles both ephemeral keypair generation and shared-secret derivation. It is consumed by the `x25519Agree` facade function and can be swapped for alternative implementations (e.g. the WebCrypto or libsodium engines from sibling packages) via the engine option on `@suaveplan/crypto`'s public API.
## Features
- Implements `AgreeEngine` for X25519 Diffie-Hellman key agreement
- Keypair generation accepts an optional `ByteSource` for deterministic testing without mocks
- X25519 internal scalar clamping means every 32-byte value is a valid secret key — no rejection-sampling loop required
- Delegates through `@suaveplan/crypto-curve/x25519` so the build-time primitive selector applies
- Returns the raw 32-byte shared secret; key derivation (HKDF, etc.) is the caller's responsibility
## Usage: Basic Example
```typescript
import { NobleX25519Engine } from "@suaveplan/crypto-core/agree/noble-agree-engine";
const engine = new NobleX25519Engine();
const alice = engine.generateKeypair();
const bob = engine.generateKeypair();
// Both sides compute the same shared secret
const aliceShared = engine.agree(alice.secretKey, bob.publicKey);
const bobShared = engine.agree(bob.secretKey, alice.publicKey);
console.log(aliceShared.length); // 32
```
## Usage: Advanced Example
```typescript
import { NobleX25519Engine } from "@suaveplan/crypto-core/agree/noble-agree-engine";
import { hkdf } from "@suaveplan/crypto-hash";
const engine = new NobleX25519Engine();
// Use a deterministic byte source for reproducible tests
const fixedBytes = (n: number) => new Uint8Array(n).fill(0xab);
const aliceKeypair = engine.generateKeypair(fixedBytes);
const bob = engine.generateKeypair();
// Derive a session key from the raw shared secret
const rawSecret = engine.agree(aliceKeypair.secretKey, bob.publicKey);
const sessionKey = hkdf("SHA-256", rawSecret, {
salt: new Uint8Array(32),
info: new TextEncoder().encode("session-key-v1"),
length: 32,
});
console.log(sessionKey.length); // 32
```
## API Reference
### `NobleX25519Engine`
```typescript
class NobleX25519Engine implements AgreeEngine
```
**`generateKeypair(byteSource?): AgreeKeypair`**
| Parameter | Type | Description |
|--------------|-------------------------|--------------------------------------------------------------|
| `byteSource` | `ByteSource \| undefined` | Optional source of random bytes. Defaults to `defaultByteSource` (secure random). |
Returns `AgreeKeypair { publicKey: Uint8Array; secretKey: Uint8Array }` where both keys are 32 bytes.
**`agree(mySecret, theirPublic): Uint8Array`**
| Parameter | Type | Description |
|---------------|--------------|-------------------------------------------|
| `mySecret` | `Uint8Array` | Local 32-byte X25519 secret key. |
| `theirPublic` | `Uint8Array` | Remote 32-byte X25519 public key. |
Returns a 32-byte raw shared secret. Throws if either key is malformed (propagated from the underlying curve implementation).
## Implementation Notes
X25519 applies a clamping operation to the scalar before performing the Diffie-Hellman multiplication. This means any 32-byte sequence is a valid secret key — the clamp adjusts bits 0, 1, 2, 7, and 255 to ensure the scalar lands in the correct subgroup. As a result, `generateKeypair` simply draws 32 bytes from the byte source and passes them directly to `getPublicKey`; no validity check or retry loop is needed.
The raw shared secret returned by `agree` has the structure of a group element, not a uniformly random key. Callers performing symmetric encryption should always pass the output through a key derivation function (HKDF is recommended) before using it as a cipher key. The higher-level `authBox` and `sealedBox` constructs in this package perform HKDF-SHA256 derivation automatically.

View File

@@ -0,0 +1,83 @@
# x25519
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Provides X25519 Elliptic-Curve Diffie-Hellman (ECDH) key agreement — key generation and shared-secret derivation — backed by `@noble/curves`. X25519 is a state-of-the-art key-agreement protocol over Curve25519 offering 128-bit security, fast constant-time scalar multiplication, and immunity to many implementation pitfalls of older curve designs. The resulting shared secret should be passed through a KDF (e.g., HKDF via `@suaveplan/crypto-hash`) before use as a symmetric key. The facade is engine-pluggable: pass `{ engine }` to substitute a WebCrypto ECDH or libsodium-backed implementation.
## Features
- Key generation using injected `ByteSource` (testable) or `crypto.getRandomValues` (production)
- X25519 scalar clamping applied internally — every 32-byte value is a valid secret key
- Returns raw 32-byte shared secret (pass through HKDF before symmetric use)
- Engine-pluggable via `AgreeEngine` interface
- `ByteSource` / deprecated `rng` option for key-generation injection
## Usage: Basic Example
```ts
import { x25519 } from "@suaveplan/crypto-core";
const alice = x25519.generateKeypair();
const bob = x25519.generateKeypair();
const shared1 = x25519.agree(alice.secretKey, bob.publicKey);
const shared2 = x25519.agree(bob.secretKey, alice.publicKey);
// shared1 deep-equals shared2
```
## Usage: Advanced Example
```ts
import { x25519 } from "@suaveplan/crypto-core";
import { hkdf } from "@suaveplan/crypto-hash";
// Key agreement + HKDF key derivation
const alice = x25519.generateKeypair();
const bob = x25519.generateKeypair();
const rawSecret = x25519.agree(alice.secretKey, bob.publicKey);
const encKey = hkdf("SHA-256", rawSecret, {
salt: new Uint8Array(32),
info: new TextEncoder().encode("enc-key"),
length: 32,
});
```
## API Reference
### `X25519Keypair`
```ts
type X25519Keypair = { publicKey: Uint8Array; secretKey: Uint8Array }
```
Both keys are 32 bytes.
### `generateKeypair(opts?)`
```ts
function generateKeypair(opts?: X25519Options): X25519Keypair
```
Generates a fresh X25519 keypair. X25519 clamps the scalar internally, so any 32 random bytes form a valid key.
| Param | Type | Description |
|-------|------|-------------|
| `opts.byteSource` | `ByteSource?` | Byte source for key material. |
| `opts.rng` | `ByteSource?` | Deprecated alias for `byteSource`. |
| `opts.engine` | `AgreeEngine?` | Override the default noble engine. |
### `agree(mySecret, theirPublic, opts?)`
```ts
function agree(mySecret: Uint8Array, theirPublic: Uint8Array, opts?: { engine?: AgreeEngine }): Uint8Array
```
Performs X25519 ECDH and returns the 32-byte shared secret. Both parties calling `agree` with their own secret key and the other party's public key will derive the same secret.
## Implementation Notes
The raw X25519 shared secret must be passed through a KDF such as HKDF before use as a symmetric encryption key. Raw DH outputs have high entropy but are not uniformly distributed and may be biased in edge cases. See `@suaveplan/crypto-hash` for `hkdf`. The `agree` function is called `getSharedSecret` in the noble library — the facade renames it for clarity in the key-agreement context.

View File

@@ -0,0 +1,103 @@
# auth-box
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
- [noble-auth-box-engine](./noble-auth-box-engine/noble-auth-box-engine.md) — <!-- begin:auto-docs --> _(no submodules)_- [auth-box](./auth-box.md) — <!-- begin:auto-docs --> _(no submodules)_- noble-auth-box-engine — <!-- begin:auto-docs --> _(n…
- [noble-auth-box-engine](./noble-auth-box-engine/noble-auth-box-engine.md) — <!-- begin:auto-docs --> _(no submodules)_- [auth-box](./auth-box.md) — <!-- begin:auto-docs --> _(no submodules)_- [noble-auth-box-engine](./noble-auth-box-engine/noble-auth-box-engine.md) — <!-- begin:auto-docs --> _(no submodules)_- [auth-box](./auth-box.md) — <!-- begin:auto-docs --> _(no submodules)_- noble-auth-box-engine — <!--…
- [noble-auth-box-engine](./noble-auth-box-engine/noble-auth-box-engine.md) — <!-- begin:auto-docs --> _(no submodules)_- [auth-box](./auth-box.md) — <!-- begin:auto-docs --> _(no submodules)_- noble-auth-box-engine — <!-- begin:auto-docs --> _(…
- [noble-auth-box-engine](./noble-auth-box-engine/noble-auth-box-engine.md) — <!-- begin:auto-docs --> _(no submodules)_- [auth-box](./auth-box.md) — <!-- begin:auto-docs --> _(no submodules)_
## Purpose
Provides authenticated sealed-box encryption following `crypto_box` semantics. Unlike the anonymous `sealedBox`, the auth-box binds the sender's identity to the ciphertext: the recipient can verify that only the holder of the sender's secret key could have produced it. The protocol uses X25519 ECDH key agreement, HKDF-SHA256 key derivation, and XChaCha20-Poly1305 encryption. Both parties must hold each other's public keys before communicating, making this suitable for protocols with established key exchange such as Signal-style messaging or server-to-server encryption.
## Features
- Authenticated sender — recipient verifies sender identity implicitly via ECDH
- X25519 ECDH + HKDF-SHA256 + XChaCha20-Poly1305 (crypto_box semantics)
- Caller-supplied 24-byte nonce for fine-grained nonce management
- Engine-pluggable via `AuthBoxEngine` interface
- Authenticated decryption — rejects tampered ciphertext with `CRYPTO.AEAD.DECRYPT_FAILED`
## Usage: Basic Example
```ts
import { authBox, x25519 } from "@suaveplan/crypto-core";
import { randomBytes } from "@suaveplan/crypto-core";
const alice = x25519.generateKeypair();
const bob = x25519.generateKeypair();
const nonce = randomBytes(24);
const msg = new TextEncoder().encode("authenticated message");
// Alice seals for Bob
const ct = authBox.seal(alice.secretKey, bob.publicKey, msg, nonce);
// Bob opens from Alice
const pt = authBox.open(alice.publicKey, bob.secretKey, ct, nonce);
```
## Usage: Advanced Example
```ts
import { authBox, x25519, randomBytes } from "@suaveplan/crypto-core";
import type { AuthBoxEngine } from "@suaveplan/crypto-core/engines";
// Custom engine stub for testing
const stubEngine: AuthBoxEngine = {
seal: (_senderSec, _recipPub, pt, _nonce) => pt.slice(),
open: (_senderPub, _recipSec, ct, _nonce) => ct.slice(),
};
const alice = x25519.generateKeypair();
const bob = x25519.generateKeypair();
const nonce = randomBytes(24);
const msg = new Uint8Array([0x42]);
const ct = authBox.seal(alice.secretKey, bob.publicKey, msg, nonce, { engine: stubEngine });
const pt = authBox.open(alice.publicKey, bob.secretKey, ct, nonce, { engine: stubEngine });
```
## API Reference
### `seal(senderSecret, recipientPublic, plaintext, nonce, opts?)`
```ts
function seal(
senderSecret: Uint8Array,
recipientPublic: Uint8Array,
plaintext: Uint8Array,
nonce: Uint8Array,
opts?: AuthBoxEngineOptions
): Uint8Array
```
Encrypts and authenticates `plaintext`. Returns the XChaCha20-Poly1305 ciphertext + 16-byte tag.
| Param | Type | Description |
|-------|------|-------------|
| `senderSecret` | `Uint8Array` | Sender's X25519 secret key (32 bytes). |
| `recipientPublic` | `Uint8Array` | Recipient's X25519 public key (32 bytes). |
| `plaintext` | `Uint8Array` | Data to encrypt. |
| `nonce` | `Uint8Array` | 24-byte nonce. MUST be unique per (senderKey, recipientKey) pair. |
| `opts.engine` | `AuthBoxEngine?` | Override the default noble engine. |
### `open(senderPublic, recipientSecret, ciphertext, nonce, opts?)`
```ts
function open(
senderPublic: Uint8Array,
recipientSecret: Uint8Array,
ciphertext: Uint8Array,
nonce: Uint8Array,
opts?: AuthBoxEngineOptions
): Uint8Array
```
Verifies and decrypts. Throws on authentication failure.
**Throws:** `CryptoError("CRYPTO.AEAD.DECRYPT_FAILED")` — tag mismatch or corrupted ciphertext.
## Implementation Notes
The HKDF info string `"suaveplan-auth-box-v1"` is domain-separated from the sealed-box protocol. Authentication works because the shared secret is derived from `ECDH(senderSecret, recipientPublic)` which equals `ECDH(recipientSecret, senderPublic)` — only the holders of the respective secret keys can compute this value. The caller is responsible for nonce uniqueness; a counter concatenated with a random prefix, or a large random nonce (24 bytes, safe for random generation), are both acceptable strategies.

View File

@@ -0,0 +1,111 @@
# NobleAuthBoxEngine
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`NobleAuthBoxEngine` implements the `AuthBoxEngine` interface using a `crypto_box`-style authenticated encryption protocol backed by `@noble/curves` and `@noble/ciphers` (via the `@suaveplan/crypto-curve` and `@suaveplan/crypto-cipher` subpaths). Unlike a sealed-box, an auth-box retains sender identity: the sender's long-term secret key is used in the ECDH step, allowing the recipient to verify that the message came from the expected party. The nonce is caller-supplied; callers are responsible for uniqueness per sender-recipient key pair.
## Features
- Implements `AuthBoxEngine` (`seal` / `open`) for two-party authenticated encryption
- Protocol: X25519 ECDH(senderSecret, recipientPublic) → HKDF-SHA256 → XChaCha20-Poly1305
- HKDF uses a fixed zero salt and a domain-separation info string `"suaveplan-auth-box-v1"`
- Decryption failures surface as `CryptoError` with code `AEAD.DECRYPT_FAILED`
- Caller supplies the nonce; the engine does not generate nonces internally
- Delegates to `@suaveplan/crypto-cipher/xchacha20-poly1305` and `@suaveplan/crypto-curve/x25519` for build-time primitive selection
## Usage: Basic Example
```typescript
import { NobleAuthBoxEngine } from "@suaveplan/crypto-core/auth-box/noble-auth-box-engine";
import { randomBytes } from "@suaveplan/crypto-core";
const engine = new NobleAuthBoxEngine();
// Generate sender and recipient keypairs (X25519)
const sender = { secretKey: randomBytes(32), publicKey: /* derive */ new Uint8Array(32) };
const recipient = { secretKey: randomBytes(32), publicKey: /* derive */ new Uint8Array(32) };
const nonce = randomBytes(24);
const plaintext = new TextEncoder().encode("hello from sender");
const ciphertext = engine.seal(sender.secretKey, recipient.publicKey, plaintext, nonce);
const recovered = engine.open(sender.publicKey, recipient.secretKey, ciphertext, nonce);
console.log(new TextDecoder().decode(recovered)); // "hello from sender"
```
## Usage: Advanced Example
```typescript
import { NobleAuthBoxEngine } from "@suaveplan/crypto-core/auth-box/noble-auth-box-engine";
import { NobleX25519Engine } from "@suaveplan/crypto-core/agree/noble-agree-engine";
import { CryptoError } from "@suaveplan/crypto-core/errors/crypto-error";
import { CRYPTO_CODES } from "@suaveplan/crypto-core/errors/codes";
import { randomBytes } from "@suaveplan/crypto-core";
const agreeEngine = new NobleX25519Engine();
const authBoxEngine = new NobleAuthBoxEngine();
const alice = agreeEngine.generateKeypair();
const bob = agreeEngine.generateKeypair();
const nonce = randomBytes(24);
const message = new TextEncoder().encode("authenticated message from alice");
// Alice seals a message to Bob
const ciphertext = authBoxEngine.seal(alice.secretKey, bob.publicKey, message, nonce);
// Bob opens it using Alice's public key for verification
const plaintext = authBoxEngine.open(alice.publicKey, bob.secretKey, ciphertext, nonce);
console.log(new TextDecoder().decode(plaintext));
// Tampered ciphertext is rejected
try {
const tampered = ciphertext.slice();
tampered[tampered.length - 1] ^= 0xff;
authBoxEngine.open(alice.publicKey, bob.secretKey, tampered, nonce);
} catch (err) {
if (err instanceof CryptoError && err.code === CRYPTO_CODES["AEAD.DECRYPT_FAILED"]) {
console.log("Tampered ciphertext correctly rejected");
}
}
```
## API Reference
### `NobleAuthBoxEngine`
```typescript
class NobleAuthBoxEngine implements AuthBoxEngine
```
**`seal(senderSecret, recipientPublic, plaintext, nonce): Uint8Array`**
| Parameter | Type | Description |
|------------------|--------------|----------------------------------------------------------|
| `senderSecret` | `Uint8Array` | Sender's 32-byte X25519 secret key. |
| `recipientPublic`| `Uint8Array` | Recipient's 32-byte X25519 public key. |
| `plaintext` | `Uint8Array` | Message bytes to encrypt. |
| `nonce` | `Uint8Array` | 24-byte nonce; must be unique per (senderKey, recipientKey) pair. |
Returns the ciphertext with a 16-byte Poly1305 authentication tag appended.
**`open(senderPublic, recipientSecret, ciphertext, nonce): Uint8Array`**
| Parameter | Type | Description |
|------------------|--------------|----------------------------------------------------------|
| `senderPublic` | `Uint8Array` | Sender's 32-byte X25519 public key. |
| `recipientSecret`| `Uint8Array` | Recipient's 32-byte X25519 secret key. |
| `ciphertext` | `Uint8Array` | Ciphertext to decrypt (must include Poly1305 tag). |
| `nonce` | `Uint8Array` | Same 24-byte nonce used during `seal`. |
Returns plaintext bytes. Throws `CryptoError` with code `AEAD.DECRYPT_FAILED` if authentication fails.
## Implementation Notes
The shared secret produced by X25519 is not uniformly random — it is a compressed curve point. Passing it directly as a cipher key would be unsafe. HKDF-SHA256 with a fixed zero salt and the domain string `"suaveplan-auth-box-v1"` stretches and randomises the material to produce a 32-byte key suitable for XChaCha20-Poly1305. The domain string provides separation from the analogous sealed-box derivation that uses `"suaveplan-sealed-box-v1"`, preventing cross-protocol confusion if the same keypair is used with both constructs.
XChaCha20-Poly1305 is preferred over ChaCha20-Poly1305 because its 24-byte nonce makes collision probability negligible over a large volume of messages with the same keypair. Even so, nonce uniqueness is the caller's responsibility — the engine does not track or enforce it.

View File

@@ -0,0 +1,89 @@
# CRYPTO_CODES
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`codes.ts` defines the complete set of stable error codes used throughout `@suaveplan/crypto`. Each code is a namespaced string constant produced by `defineCodes` from `@suaveplan/error`, ensuring that codes are unique within the `"CRYPTO"` namespace and that the `CryptoCode` union type is always in sync with the registered values. Consumers can import `CRYPTO_CODES` and branch on `error.code` without inspecting message strings, making error handling robust across version upgrades.
## Features
- Seven codes covering the four main failure domains: key validity, AEAD nonce/decrypt, signing, and RNG
- Generated via `defineCodes("CRYPTO", [...])` which prefixes each entry and freezes the resulting object
- `CryptoCode` is derived as a `typeof` union so TypeScript narrows exhaustively in switch statements
- No runtime dependencies beyond `@suaveplan/error`
## Usage: Basic Example
```typescript
import { CRYPTO_CODES } from "@suaveplan/crypto-core/errors/codes";
import { CryptoError } from "@suaveplan/crypto-core/errors/crypto-error";
throw new CryptoError(
CRYPTO_CODES["RNG.UNAVAILABLE"],
"error.crypto.rng.unavailable"
);
```
## Usage: Advanced Example
```typescript
import { CRYPTO_CODES, type CryptoCode } from "@suaveplan/crypto-core/errors/codes";
import { CryptoError } from "@suaveplan/crypto-core/errors/crypto-error";
function handleCryptoError(err: CryptoError): string {
switch (err.code as CryptoCode) {
case CRYPTO_CODES["AEAD.DECRYPT_FAILED"]:
return "Decryption failed — message may be corrupted or the key is wrong.";
case CRYPTO_CODES["SIGN.VERIFY_FAILED"]:
return "Signature verification failed — message may have been tampered with.";
case CRYPTO_CODES["KEY.INVALID_LENGTH"]:
return "Supplied key has an incorrect byte length for this algorithm.";
case CRYPTO_CODES["AEAD.INVALID_NONCE"]:
return "Nonce does not meet the length requirements for the chosen cipher.";
case CRYPTO_CODES["SIGN.INVALID_KEY"]:
return "Signing key is malformed or does not correspond to the expected curve.";
case CRYPTO_CODES["RNG.INVALID_LENGTH"]:
return "Requested byte count is outside the permissible range.";
case CRYPTO_CODES["RNG.UNAVAILABLE"]:
return "Cryptographically secure random source is not available in this environment.";
default:
return "Unknown crypto error.";
}
}
```
## API Reference
### `CRYPTO_CODES`
```typescript
const CRYPTO_CODES: Readonly<Record<string, string>>
```
A frozen map from short code keys to their namespaced string values. All values are prefixed with `"CRYPTO."` by `defineCodes`.
| Key | Namespaced value | When thrown |
|------------------------|-------------------------------|-------------------------------------------------------|
| `KEY.INVALID_LENGTH` | `"CRYPTO.KEY.INVALID_LENGTH"` | A key byte length does not match algorithm requirements |
| `AEAD.INVALID_NONCE` | `"CRYPTO.AEAD.INVALID_NONCE"` | Nonce byte length is wrong for the selected cipher |
| `AEAD.DECRYPT_FAILED` | `"CRYPTO.AEAD.DECRYPT_FAILED"` | Authentication tag verification failed during decrypt |
| `SIGN.INVALID_KEY` | `"CRYPTO.SIGN.INVALID_KEY"` | Signing key is malformed |
| `SIGN.VERIFY_FAILED` | `"CRYPTO.SIGN.VERIFY_FAILED"` | Signature does not match the message |
| `RNG.INVALID_LENGTH` | `"CRYPTO.RNG.INVALID_LENGTH"` | Byte count requested from RNG is invalid |
| `RNG.UNAVAILABLE` | `"CRYPTO.RNG.UNAVAILABLE"` | Secure random source is absent (rare, environment issue) |
### `CryptoCode`
```typescript
type CryptoCode = (typeof CRYPTO_CODES)[keyof typeof CRYPTO_CODES];
```
Union of all namespaced code strings. Use this type to annotate `err.code` when narrowing `CryptoError` in typed catch blocks.
## Implementation Notes
`defineCodes` returns a plain frozen object. The `CryptoCode` type is derived with `typeof CRYPTO_CODES[keyof typeof CRYPTO_CODES]`, which collapses to a union of string literals. This approach means TypeScript will warn at compile time if a switch statement is missing a case when the union is used with `noUncheckedIndexedAccess` and strict narrowing enabled.
The `"AEAD.DECRYPT_FAILED"` code is reused for both authenticated-encryption failures and protocol-level failures (e.g. truncated sealed-box ciphertext), because both represent the same caller-facing outcome: the ciphertext could not be opened.

View File

@@ -0,0 +1,81 @@
# CryptoError
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`CryptoError` is the package-level error class for `@suaveplan/crypto`. It extends `SuaveplanError` with the `CryptoCode` discriminant, giving every thrown error a stable, namespaced code that consumers can branch on without parsing message strings. All encryption, decryption, signing, and key-agreement failures surface as `CryptoError` instances so catch handlers can distinguish crypto faults from unrelated infrastructure errors.
## Features
- Extends `SuaveplanError<CryptoCode>` for consistent error taxonomy across the `@suaveplan/*` ecosystem
- Accepts the full `SuaveplanErrorOptions` bag (`cause`, `context`, `redact`) for cause-chain wrapping and structured context attachment
- Constructor is `public`, overriding the `protected` base, so external engine adapters in sibling packages can throw typed `CryptoError` values directly
- Compatible with OTel span-event adapters defined on `SuaveplanError`
- Wire-serialisable via the standard `SuaveplanError` Zod schema
## Usage: Basic Example
```typescript
import { CryptoError } from "@suaveplan/crypto-core/errors/crypto-error";
import { CRYPTO_CODES } from "@suaveplan/crypto-core/errors/codes";
throw new CryptoError(
CRYPTO_CODES["AEAD.DECRYPT_FAILED"],
"error.crypto.aead.decrypt-failed"
);
```
## Usage: Advanced Example
```typescript
import { CryptoError } from "@suaveplan/crypto-core/errors/crypto-error";
import { CRYPTO_CODES } from "@suaveplan/crypto-core/errors/codes";
function decryptPayload(
key: Uint8Array,
nonce: Uint8Array,
ciphertext: Uint8Array
): Uint8Array {
try {
return lowLevelDecrypt(key, nonce, ciphertext);
} catch (cause) {
throw new CryptoError(
CRYPTO_CODES["AEAD.DECRYPT_FAILED"],
"error.crypto.aead.decrypt-failed",
{
cause,
context: {
ciphertextLength: ciphertext.length,
nonceLength: nonce.length,
},
}
);
}
}
```
## API Reference
### `CryptoError`
```typescript
class CryptoError extends SuaveplanError<CryptoCode>
```
**Constructor**
| Parameter | Type | Description |
|--------------|------------------------|----------------------------------------------------------|
| `code` | `CryptoCode` | Stable namespaced error code from `CRYPTO_CODES`. |
| `messageKey` | `string` | i18n resolver key (e.g. `"error.crypto.aead.decrypt-failed"`). |
| `options` | `SuaveplanErrorOptions \| undefined` | Optional `cause`, `context`, and `redact` fields. |
All `SuaveplanError` instance methods are inherited, including `toJSON()`, `toWireFormat()`, and OTel helpers.
## Implementation Notes
The constructor body is intentionally trivial — it exists solely to widen the `protected` base constructor to `public`. This pattern is required because `SuaveplanError`'s constructor is `protected` (it enforces that error classes always provide a discriminating `code`), but engine adapters outside this package's internal module graph need to throw `CryptoError` directly. The biome lint suppression comment is included to silence the `noUselessConstructor` rule, which would otherwise flag the no-op forwarding call as dead code.
The class does not add any new instance properties or methods on top of `SuaveplanError`. All richness — cause-chain, context bag, i18n, wire format — lives in the base class and flows through automatically.

View File

@@ -0,0 +1,72 @@
# constant-time-equal
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Compares two `Uint8Array` values in constant time to prevent timing side-channels. When comparing secrets such as authentication tags, HMAC outputs, or session tokens, a naive byte-by-byte comparison that returns early on the first mismatch leaks information about how many leading bytes matched — an attacker measuring response latency can exploit this to forge values one byte at a time. This implementation uses the XOR-accumulate pattern to ensure that branch-sensitive content (byte values) never causes an early return.
## Features
- Branch-free for content bytes — only the length check short-circuits (length is not secret in AEAD contexts)
- O(min(len(a), len(b))) — no early exit on byte mismatch
- Returns `true` only when lengths are equal AND all bytes match
- Zero dependencies, pure TypeScript
## Usage: Basic Example
```ts
import { constantTimeEqual } from "@suaveplan/crypto-core/primitives/constant-time-equal";
const tag1 = new Uint8Array([0x01, 0x02, 0x03]);
const tag2 = new Uint8Array([0x01, 0x02, 0x03]);
constantTimeEqual(tag1, tag2); // true
```
## Usage: Advanced Example
```ts
import { constantTimeEqual } from "@suaveplan/crypto-core/primitives/constant-time-equal";
import { aeadDecrypt, randomBytes } from "@suaveplan/crypto-core";
// Verify a server-generated token matches the expected token
// without leaking information via timing differences
function verifyToken(received: Uint8Array, expected: Uint8Array): boolean {
return constantTimeEqual(received, expected);
}
// Safe for use with auth tags, HMAC outputs, session tokens
const a = new Uint8Array(32).fill(0xff);
const b = new Uint8Array(32).fill(0xff);
b[31] = 0xfe; // single-bit difference at the end
// Returns false, but execution time is the same as if all bytes matched
verifyToken(a, b); // false
```
## API Reference
### `constantTimeEqual(a, b)`
```ts
function constantTimeEqual(a: Uint8Array, b: Uint8Array): boolean
```
Returns `true` iff `a.length === b.length` and every byte of `a` equals the corresponding byte of `b`.
| Param | Type | Description |
|-------|------|-------------|
| `a` | `Uint8Array` | First buffer. |
| `b` | `Uint8Array` | Second buffer to compare against. |
**Returns:** `boolean` — `true` when buffers are identical, `false` otherwise.
**Does not throw.**
## Implementation Notes
The XOR-accumulate pattern (`diff |= a[i] ^ b[i]`) ensures the loop body never branches on byte values. At the end, `diff === 0` iff every XOR was zero (i.e., all bytes matched). The length check at the start does short-circuit, but buffer length is not considered secret in standard AEAD or HMAC comparison contexts — lengths are visible from ciphertext structure. If your protocol requires hiding lengths, pad the buffers before calling this function.
JavaScript's JIT may theoretically produce variable-timing code despite the branch-free pattern; production deployments should combine this with server-side rate limiting to bound oracle value.

View File

@@ -0,0 +1,97 @@
# random-bytes
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Provides the single authorised callsite for `crypto.getRandomValues` within `@suaveplan/crypto`. Every other module that needs random bytes calls `randomBytes()` or accepts a `ByteSource` injection — this design concentrates the randomness surface to one file, making it trivially auditable and testable. The `ByteSource` type replaces the old `Rng` alias (which conflicted with `@suaveplan/types`'s number-producing `Rng` interface) while preserving backwards compatibility via a deprecated re-export.
## Features
- `ByteSource = (n: number) => Uint8Array` — the injected randomness type used across key-generation functions
- `Rng` deprecated alias for `ByteSource` for main-branch migration
- `defaultByteSource` — calls `crypto.getRandomValues`; works in Bun, Node, and browsers
- `defaultRng` — deprecated alias for `defaultByteSource`
- `randomBytes(n, opts?)` — validated facade that rejects non-integer or negative `n`
- `RandomBytesOptions.byteSource` for deterministic injection; `rng` kept as deprecated alias
## Usage: Basic Example
```ts
import { randomBytes } from "@suaveplan/crypto-core/primitives/random-bytes";
const key = randomBytes(32); // 32 random bytes, CSPRNG-backed
```
## Usage: Advanced Example
```ts
import { randomBytes, type ByteSource } from "@suaveplan/crypto-core/primitives/random-bytes";
// Deterministic source for reproducible tests
let counter = 0;
const seeded: ByteSource = (n) => {
const out = new Uint8Array(n);
for (let i = 0; i < n; i++) out[i] = (counter++ & 0xff);
return out;
};
const key = randomBytes(32, { byteSource: seeded });
```
## API Reference
### `ByteSource`
```ts
type ByteSource = (n: number) => Uint8Array
```
A function that returns `n` cryptographically-suitable bytes. The default implementation calls `crypto.getRandomValues`.
### `Rng` (deprecated)
```ts
/** @deprecated */ type Rng = ByteSource
```
Backwards-compatibility alias. Prefer `ByteSource` in new code.
### `defaultByteSource`
```ts
const defaultByteSource: ByteSource
```
The production byte source. Wraps `crypto.getRandomValues`. Works in Bun, Node ≥18, and browsers.
### `defaultRng` (deprecated)
```ts
/** @deprecated */ const defaultRng: ByteSource
```
Alias for `defaultByteSource`.
### `randomBytes(n, options?)`
```ts
function randomBytes(n: number, options?: RandomBytesOptions): Uint8Array
```
Returns `n` random bytes. Throws `CryptoError("CRYPTO.RNG.INVALID_LENGTH")` if `n` is not a non-negative integer.
| Param | Type | Description |
|-------|------|-------------|
| `n` | `number` | Number of bytes to generate. Must be a non-negative integer. |
| `options.byteSource` | `ByteSource?` | Override the default source. |
| `options.rng` | `ByteSource?` | Deprecated alias for `byteSource`. |
**Throws:** `CryptoError("CRYPTO.RNG.INVALID_LENGTH")` — `n` is negative, non-integer, or NaN.
## Implementation Notes
The `crypto.getRandomValues` restriction is enforced at code review and by `tests/no-disallowed.test.ts`, which walks every `.ts` source file under `src/` and asserts that `crypto.getRandomValues` appears only in this file. This guards against accidental randomness leakage in library code that should be injecting `ByteSource` for testability.
The `ByteSource` / `Rng` rename was necessary because `@suaveplan/types` exports `Rng = { nextUint32(): number; nextFloat(): number }` — a fundamentally different interface. Shipping both would cause confusing type errors for consumers who import from both packages.

View File

@@ -0,0 +1,68 @@
# zeroize
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Overwrites a `Uint8Array` buffer with zeros in-place to minimise the lifetime of sensitive data on the JavaScript heap. Callers should invoke `zeroize` as soon as a secret key, derived key, or plaintext is no longer needed. While JavaScript's garbage collector may have already relocated the backing `ArrayBuffer` to a new memory region — making this a best-effort operation — it is still considered good practice because it reduces the window during which a memory-inspection attack or heap dump could expose the sensitive bytes.
## Features
- Fills every byte of the buffer with `0x00` using `Uint8Array.fill`
- In-place mutation — no allocation, no return value
- Works on any `Uint8Array` subview regardless of byte offset or length
- Zero dependencies
## Usage: Basic Example
```ts
import { zeroize } from "@suaveplan/crypto-core/primitives/zeroize";
const secretKey = new Uint8Array(32);
// ... use secretKey for cryptographic operations ...
zeroize(secretKey); // clear sensitive bytes
```
## Usage: Advanced Example
```ts
import { zeroize } from "@suaveplan/crypto-core/primitives/zeroize";
import { randomBytes } from "@suaveplan/crypto-core/primitives/random-bytes";
import { aeadEncrypt } from "@suaveplan/crypto-core/aead";
function encryptAndForget(plaintext: Uint8Array): Uint8Array {
const key = randomBytes(32);
const nonce = randomBytes(12);
try {
return aeadEncrypt("AES-256-GCM", key, plaintext, { nonce });
} finally {
// Always clear keys even if encrypt throws
zeroize(key);
zeroize(nonce);
}
}
```
## API Reference
### `zeroize(buf)`
```ts
function zeroize(buf: Uint8Array): void
```
Fills every byte of `buf` with `0x00` in-place.
| Param | Type | Description |
|-------|------|-------------|
| `buf` | `Uint8Array` | Buffer to zero out. Modified in-place. |
**Returns:** `void`
**Does not throw.** Calling on a zero-length buffer is a no-op.
## Implementation Notes
`Uint8Array.fill(0)` is the most direct approach. Some runtimes optimise this to a `memset` call at the C level, which is typically not subject to compiler dead-store elimination. However, JavaScript engines may still optimise away stores to unreachable buffers — this is a known limitation of in-process zeroing in managed runtimes. For high-security environments where in-process zeroing is insufficient, consider using hardware security modules or OS-level secure memory (not available from pure JavaScript).

View File

@@ -0,0 +1,117 @@
# NobleSealedBoxEngine
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`NobleSealedBoxEngine` implements the `SealedBoxEngine` interface using a `crypto_box_seal`-style anonymous-sender encryption protocol. The sender generates a fresh ephemeral X25519 keypair per message, performs ECDH with the recipient's long-term public key, derives a symmetric key via HKDF-SHA256, and encrypts with XChaCha20-Poly1305. The ephemeral public key and nonce are prepended to the output so the recipient can recover the shared secret without any out-of-band communication. Sender anonymity is preserved because the ephemeral secret key is discarded immediately after sealing.
## Features
- Implements `SealedBoxEngine` (`seal` / `open`) for anonymous authenticated encryption
- Protocol output layout: `ephemeralPub (32) || nonce (24) || ciphertext || tag (16)`
- HKDF-SHA256 key derivation with zero salt and info `"suaveplan-sealed-box-v1"` for domain separation
- Ephemeral keypair generated fresh per `seal` call using `defaultByteSource` (secure random)
- Minimum ciphertext length validated during `open`; short ciphertexts throw `CryptoError` before attempting decryption
- All decryption failures wrapped in `CryptoError` with code `AEAD.DECRYPT_FAILED`
## Usage: Basic Example
```typescript
import { NobleSealedBoxEngine } from "@suaveplan/crypto-core/sealed-box/noble-sealed-box-engine";
import { NobleX25519Engine } from "@suaveplan/crypto-core/agree/noble-agree-engine";
const agreeEngine = new NobleX25519Engine();
const sealedBoxEngine = new NobleSealedBoxEngine();
const recipient = agreeEngine.generateKeypair();
// Any party can seal to recipient without knowing sender's identity
const plaintext = new TextEncoder().encode("anonymous message");
const ciphertext = sealedBoxEngine.seal(recipient.publicKey, plaintext);
// Only recipient can open
const recovered = sealedBoxEngine.open(
recipient.secretKey,
recipient.publicKey,
ciphertext
);
console.log(new TextDecoder().decode(recovered)); // "anonymous message"
```
## Usage: Advanced Example
```typescript
import { NobleSealedBoxEngine } from "@suaveplan/crypto-core/sealed-box/noble-sealed-box-engine";
import { NobleX25519Engine } from "@suaveplan/crypto-core/agree/noble-agree-engine";
import { CryptoError } from "@suaveplan/crypto-core/errors/crypto-error";
import { CRYPTO_CODES } from "@suaveplan/crypto-core/errors/codes";
const agreeEngine = new NobleX25519Engine();
const engine = new NobleSealedBoxEngine();
const recipient = agreeEngine.generateKeypair();
// Each seal call produces a different ciphertext (fresh ephemeral key + nonce)
const msg = new TextEncoder().encode("one-time sealed value");
const ct1 = engine.seal(recipient.publicKey, msg);
const ct2 = engine.seal(recipient.publicKey, msg);
console.log(ct1.length === ct2.length); // true (same size)
// Truncated ciphertext is caught before decryption attempt
try {
engine.open(recipient.secretKey, recipient.publicKey, new Uint8Array(10));
} catch (err) {
if (err instanceof CryptoError && err.code === CRYPTO_CODES["AEAD.DECRYPT_FAILED"]) {
console.log("Short ciphertext rejected early");
}
}
// Tampered ciphertext also fails at authentication
try {
const tampered = ct1.slice();
tampered[40] ^= 0x01;
engine.open(recipient.secretKey, recipient.publicKey, tampered);
} catch (err) {
if (err instanceof CryptoError && err.code === CRYPTO_CODES["AEAD.DECRYPT_FAILED"]) {
console.log("Tampered ciphertext rejected");
}
}
```
## API Reference
### `NobleSealedBoxEngine`
```typescript
class NobleSealedBoxEngine implements SealedBoxEngine
```
**`seal(recipientPublic, plaintext): Uint8Array`**
| Parameter | Type | Description |
|-------------------|--------------|--------------------------------------------------------|
| `recipientPublic` | `Uint8Array` | Recipient's 32-byte X25519 public key. |
| `plaintext` | `Uint8Array` | Message bytes to encrypt. |
Returns a self-contained blob: `ephemeralPublicKey (32) || nonce (24) || ciphertext || poly1305Tag (16)`. Total overhead is 72 bytes beyond plaintext length.
**`open(recipientSecret, recipientPublic, ciphertext): Uint8Array`**
| Parameter | Type | Description |
|-------------------|--------------|----------------------------------------------------------------|
| `recipientSecret` | `Uint8Array` | Recipient's 32-byte X25519 secret key. |
| `recipientPublic` | `Uint8Array` | Recipient's 32-byte X25519 public key (currently unused but kept for API symmetry). |
| `ciphertext` | `Uint8Array` | Sealed blob produced by `seal`. |
Returns plaintext bytes. Throws `CryptoError` (`AEAD.DECRYPT_FAILED`) if:
- The ciphertext is shorter than 72 bytes (minimum: 32 ephKey + 24 nonce + 16 tag)
- Authentication tag verification fails
## Implementation Notes
The `_recipientPublic` parameter in `open` is accepted to keep the `SealedBoxEngine` interface consistent with engines that do require it (e.g. the libsodium adapter), but is not used in the computation — the recipient public key is only needed to reconstruct the shared secret on the _sender_ side. The Noble implementation derives everything from `recipientSecret` and the extracted `ephemeralPublic` alone.
Each `seal` call draws fresh randomness for both the ephemeral secret (32 bytes) and the XChaCha20-Poly1305 nonce (24 bytes) from `defaultByteSource`. This means two invocations with identical plaintext and recipient key will always produce different ciphertexts — forward-secrecy is automatic. The trade-off is that the nonce does not need to be managed by the caller for this primitive, unlike `authBox` where nonce uniqueness is the caller's responsibility.

View File

@@ -0,0 +1,94 @@
# sealed-box
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
- [noble-sealed-box-engine](./noble-sealed-box-engine/noble-sealed-box-engine.md) — <!-- begin:auto-docs --> _(no submodules)_- [sealed-box](./sealed-box.md) — <!-- begin:auto-docs --> _(no submodules)_- noble-sealed-box-engine — <!-- begin:auto-docs --> _…
- [noble-sealed-box-engine](./noble-sealed-box-engine/noble-sealed-box-engine.md) — <!-- begin:auto-docs --> _(no submodules)_- [sealed-box](./sealed-box.md) — <!-- begin:auto-docs --> _(no submodules)_- [noble-sealed-box-engine](./noble-sealed-box-engine/noble-sealed-box-engine.md) — <!-- begin:auto-docs --> _(no submodules)_- [sealed-box](./sealed-box.md) — <!-- begin:auto-docs --> _(no submodules)_- noble-sealed-box-engine — <!…
- [noble-sealed-box-engine](./noble-sealed-box-engine/noble-sealed-box-engine.md) — <!-- begin:auto-docs --> _(no submodules)_- [sealed-box](./sealed-box.md) — <!-- begin:auto-docs --> _(no submodules)_- noble-sealed-box-engine — <!-- begin:auto-docs -->…
- [noble-sealed-box-engine](./noble-sealed-box-engine/noble-sealed-box-engine.md) — <!-- begin:auto-docs --> _(no submodules)_- [sealed-box](./sealed-box.md) — <!-- begin:auto-docs --> _(no submodules)_
## Purpose
Provides anonymous-sender sealed-box encryption following `crypto_box_seal` semantics. The sender generates an ephemeral X25519 keypair, performs ECDH with the recipient's public key, derives a symmetric key via HKDF-SHA256, then encrypts with XChaCha20-Poly1305. The ephemeral public key is prepended to the ciphertext blob so the recipient can derive the same symmetric key without the sender needing to reveal their identity. This pattern is commonly used for one-way encrypted messages where sender anonymity is desired.
## Features
- Anonymous sender — no sender keypair required
- X25519 ECDH + HKDF-SHA256 + XChaCha20-Poly1305 (crypto_box_seal semantics)
- Self-contained output: `ephemeralPub (32) || nonce (24) || ciphertext || tag (16)`
- Engine-pluggable via `SealedBoxEngine` interface
- Authenticated decryption — rejects tampered ciphertext with `CRYPTO.AEAD.DECRYPT_FAILED`
## Usage: Basic Example
```ts
import { sealedBox } from "@suaveplan/crypto-core";
import { x25519 } from "@suaveplan/crypto-core";
const recipient = x25519.generateKeypair();
// Seal (any sender, no keypair needed)
const ciphertext = sealedBox.seal(recipient.publicKey, new TextEncoder().encode("secret"));
// Open (recipient only)
const plaintext = sealedBox.open(recipient.secretKey, recipient.publicKey, ciphertext);
```
## Usage: Advanced Example
```ts
import { sealedBox } from "@suaveplan/crypto-core";
import { x25519 } from "@suaveplan/crypto-core";
import type { SealedBoxEngine } from "@suaveplan/crypto-core/engines";
// Custom engine for testing (deterministic, not secure)
const stubEngine: SealedBoxEngine = {
seal: (_pub, pt) => pt.slice(),
open: (_sec, _pub, ct) => ct.slice(),
};
const recipient = x25519.generateKeypair();
const msg = new Uint8Array([1, 2, 3]);
const ct = sealedBox.seal(recipient.publicKey, msg, { engine: stubEngine });
const pt = sealedBox.open(recipient.secretKey, recipient.publicKey, ct, { engine: stubEngine });
```
## API Reference
### `seal(recipientPublic, plaintext, opts?)`
```ts
function seal(
recipientPublic: Uint8Array,
plaintext: Uint8Array,
opts?: SealedBoxEngineOptions
): Uint8Array
```
Encrypts and authenticates `plaintext` for the recipient. Returns a self-contained blob containing the ephemeral public key, nonce, ciphertext, and authentication tag.
| Param | Type | Description |
|-------|------|-------------|
| `recipientPublic` | `Uint8Array` | Recipient's X25519 public key (32 bytes). |
| `plaintext` | `Uint8Array` | Data to encrypt. |
| `opts.engine` | `SealedBoxEngine?` | Override the default noble engine. |
### `open(recipientSecret, recipientPublic, ciphertext, opts?)`
```ts
function open(
recipientSecret: Uint8Array,
recipientPublic: Uint8Array,
ciphertext: Uint8Array,
opts?: SealedBoxEngineOptions
): Uint8Array
```
Decrypts a sealed box. Throws on authentication failure.
**Throws:** `CryptoError("CRYPTO.AEAD.DECRYPT_FAILED")` — ciphertext is too short, or authentication tag verification fails.
## Implementation Notes
The HKDF info string `"suaveplan-sealed-box-v1"` is domain-separated to prevent key reuse across protocols. A zero salt is used because the X25519 shared secret already provides sufficient entropy for HKDF extraction. The nonce is randomly generated per seal operation and stored in the ciphertext blob, so the same key can be reused across many messages without nonce-reuse risk. The 24-byte XChaCha20-Poly1305 nonce is large enough that random generation is safe for up to 2^96 messages per recipient key before birthday collisions become plausible.

View File

@@ -0,0 +1,89 @@
# ecdsa-p256
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Provides ECDSA-P256 digital signature operations — key generation, signing, and verification — backed by `@noble/curves`. ECDSA over NIST P-256 (secp256r1) is widely deployed in TLS certificates, WebAuthn, and enterprise PKI infrastructure. Signatures are computed over the SHA-256 digest of the message and encoded in compact (r||s) 64-byte form. The facade is engine-pluggable: pass `{ engine }` to substitute a WebCrypto-backed implementation that may leverage hardware acceleration.
## Features
- Key generation with P-256 scalar validation retry loop (not all 32-byte values are valid scalars)
- Signs over SHA-256 digest — callers do not need to pre-hash
- Compact 64-byte (r||s) signature format
- Verification returns `boolean`, never throws on malformed input
- Engine-pluggable via `SignEngine` interface
- `ByteSource` / deprecated `rng` option for key-generation injection
## Usage: Basic Example
```ts
import { ecdsaP256 } from "@suaveplan/crypto-core";
const keypair = ecdsaP256.generateKeypair();
const message = new TextEncoder().encode("document content");
const sig = ecdsaP256.sign(keypair.secretKey, message);
const ok = ecdsaP256.verify(keypair.publicKey, sig, message);
// ok === true
```
## Usage: Advanced Example
```ts
import { ecdsaP256 } from "@suaveplan/crypto-core";
import type { ByteSource } from "@suaveplan/crypto-core/primitives/random-bytes";
// Deterministic key generation for testing
const seeded: ByteSource = (() => {
let v = 1;
return (n) => {
const out = new Uint8Array(n);
for (let i = 0; i < n; i++) out[i] = (v++ & 0xff);
return out;
};
})();
const kp = ecdsaP256.generateKeypair({ byteSource: seeded });
const msg = new Uint8Array([0xde, 0xad]);
const sig = ecdsaP256.sign(kp.secretKey, msg);
console.log(ecdsaP256.verify(kp.publicKey, sig, msg)); // true
```
## API Reference
### `EcdsaP256Keypair`
```ts
type EcdsaP256Keypair = { publicKey: Uint8Array; secretKey: Uint8Array }
```
Public key is a 33-byte compressed point; secret key is a 32-byte scalar.
### `generateKeypair(opts?)`
```ts
function generateKeypair(opts?: EcdsaP256Options): EcdsaP256Keypair
```
Generates a fresh keypair. Retries key generation until the random bytes form a valid P-256 private scalar (probability of retry is negligible — about 2^-128).
### `sign(secretKey, message, opts?)`
```ts
function sign(secretKey: Uint8Array, message: Uint8Array, opts?: { engine?: SignEngine }): Uint8Array
```
Signs `SHA-256(message)` with `secretKey`. Returns a compact 64-byte (r||s) signature.
### `verify(publicKey, signature, message, opts?)`
```ts
function verify(publicKey: Uint8Array, signature: Uint8Array, message: Uint8Array, opts?: { engine?: SignEngine }): boolean
```
Verifies compact signature against `SHA-256(message)`. Returns `false` (never throws) on malformed inputs.
## Implementation Notes
P-256 private key generation requires checking that the random scalar is in `[1, n-1]` where `n` is the curve order. The probability that a uniformly random 32-byte value is out of range is approximately `1 - n/2^256 ≈ 2^-128`, so the retry loop terminates in one iteration with overwhelming probability. The SHA-256 pre-hash is applied by the facade (not the noble library's internal hashing) to keep the engine interface consistent with Ed25519 where the full message is hashed internally. Compact signature format omits the DER encoding overhead and is interoperable with most modern P-256 consumers.

View File

@@ -0,0 +1,92 @@
# ed25519
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Provides Ed25519 digital signature operations — key generation, signing, and verification — backed by `@noble/curves`. Ed25519 is a high-speed elliptic curve signature algorithm over Curve25519, offering 128-bit security with compact 64-byte signatures and 32-byte keys. It is deterministic (no per-signature randomness needed) and is the recommended choice for new protocols. The facade is engine-pluggable: pass `{ engine }` to substitute a WebCrypto or libsodium-backed implementation.
## Features
- Key generation using injected `ByteSource` (testable) or `crypto.getRandomValues` (production)
- Deterministic signing — same key + message always produces the same signature
- Verification returns `boolean`, never throws on malformed input
- Engine-pluggable via `SignEngine` interface
- `ByteSource` / deprecated `rng` option for key-generation injection
## Usage: Basic Example
```ts
import { ed25519 } from "@suaveplan/crypto-core";
const keypair = ed25519.generateKeypair();
const message = new TextEncoder().encode("hello");
const sig = ed25519.sign(keypair.secretKey, message);
const ok = ed25519.verify(keypair.publicKey, sig, message);
// ok === true
```
## Usage: Advanced Example
```ts
import { ed25519 } from "@suaveplan/crypto-core";
import type { SignEngine } from "@suaveplan/crypto-core/engines";
import type { ByteSource } from "@suaveplan/crypto-core/primitives/random-bytes";
// Deterministic key generation for reproducible tests
let seed = 0;
const seeded: ByteSource = (n) => {
const out = new Uint8Array(n);
for (let i = 0; i < n; i++) out[i] = (seed++ & 0xff);
return out;
};
const kp = ed25519.generateKeypair({ byteSource: seeded });
const msg = new Uint8Array([1, 2, 3]);
const sig = ed25519.sign(kp.secretKey, msg);
console.log(ed25519.verify(kp.publicKey, sig, msg)); // true
console.log(ed25519.verify(kp.publicKey, sig, new Uint8Array([99]))); // false
```
## API Reference
### `Ed25519Keypair`
```ts
type Ed25519Keypair = { publicKey: Uint8Array; secretKey: Uint8Array }
```
### `generateKeypair(opts?)`
```ts
function generateKeypair(opts?: Ed25519Options): Ed25519Keypair
```
Generates a fresh keypair. Every 32-byte value is a valid Ed25519 secret key, so no retry loop is needed.
| Param | Type | Description |
|-------|------|-------------|
| `opts.byteSource` | `ByteSource?` | Byte source for key material. |
| `opts.rng` | `ByteSource?` | Deprecated alias for `byteSource`. |
| `opts.engine` | `SignEngine?` | Override the default noble engine. |
### `sign(secretKey, message, opts?)`
```ts
function sign(secretKey: Uint8Array, message: Uint8Array, opts?: { engine?: SignEngine }): Uint8Array
```
Signs `message` with `secretKey`. Returns a 64-byte signature.
### `verify(publicKey, signature, message, opts?)`
```ts
function verify(publicKey: Uint8Array, signature: Uint8Array, message: Uint8Array, opts?: { engine?: SignEngine }): boolean
```
Verifies `signature` over `message` against `publicKey`. Returns `false` (never throws) on malformed inputs.
## Implementation Notes
Ed25519 uses the Ristretto255 group internally in some implementations; `@noble/curves` uses the standard Edwards25519 form. The 64-byte signature is the standard (R, S) encoding where R is a compressed point and S is a scalar. Noble's `verify` returns `false` for many malformed inputs that other libraries throw on — the facade wraps the call in a try/catch to guarantee boolean-only returns from all implementations.

View File

@@ -0,0 +1,117 @@
# NobleEd25519Engine / NobleEcdsaP256Engine
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
This module provides two `SignEngine` implementations for the `@suaveplan/crypto` facade: `NobleEd25519Engine` for Ed25519 and `NobleEcdsaP256Engine` for ECDSA-P256. Both delegate through `@suaveplan/crypto-curve` subpaths, ensuring the build-time primitive selector in `crypto.config.ts` applies uniformly. The engines are consumed by the `ed25519Sign`, `ed25519Verify`, `ecdsaP256Sign`, and `ecdsaP256Verify` facade functions, and can be supplied directly via engine options for testing or custom engine composition.
## Features
- `NobleEd25519Engine`: implements `SignEngine` for Ed25519 (64-byte signatures, 32-byte keys)
- `NobleEcdsaP256Engine`: implements `SignEngine` for ECDSA-P256 (compact 64-byte r‖s signatures over SHA-256 digests)
- Both accept an optional `ByteSource` in `generateKeypair` for deterministic test key generation
- Ed25519 key generation needs no retry loop — every 32-byte value is a valid secret
- P-256 key generation uses `p256.randomPrivateKey()` which internally handles the rejection-sampling retry; injected byte sources bypass this and must supply a valid scalar
- ECDSA signs over the SHA-256 digest of the message, so callers do not need to pre-hash
- Delegates through `@suaveplan/crypto-curve/ed25519` and `@suaveplan/crypto-curve/p256`
## Usage: Basic Example
```typescript
import { NobleEd25519Engine } from "@suaveplan/crypto-core/sign/noble-sign-engine";
const engine = new NobleEd25519Engine();
const keypair = engine.generateKeypair();
const message = new TextEncoder().encode("hello world");
const signature = engine.sign(keypair.secretKey, message);
const valid = engine.verify(keypair.publicKey, signature, message);
console.log(valid); // true
```
## Usage: Advanced Example
```typescript
import {
NobleEd25519Engine,
NobleEcdsaP256Engine,
} from "@suaveplan/crypto-core/sign/noble-sign-engine";
// Ed25519 with a deterministic byte source
const fixedSeed = (n: number) => new Uint8Array(n).fill(0x42);
const ed25519Engine = new NobleEd25519Engine();
const deterministicKeypair = ed25519Engine.generateKeypair(fixedSeed);
// ECDSA-P256 — signs over SHA-256 digest internally
const p256Engine = new NobleEcdsaP256Engine();
const p256Keypair = p256Engine.generateKeypair();
const message = new TextEncoder().encode("transaction payload");
const sig256 = p256Engine.sign(p256Keypair.secretKey, message);
// Verification returns boolean — does not throw on invalid signatures
const ok = p256Engine.verify(p256Keypair.publicKey, sig256, message);
console.log(ok); // true
// Cross-engine verification fails gracefully
const wrongSig = ed25519Engine.sign(deterministicKeypair.secretKey, message);
const crossOk = p256Engine.verify(p256Keypair.publicKey, wrongSig, message);
console.log(crossOk); // false
```
## API Reference
### `NobleEd25519Engine`
```typescript
class NobleEd25519Engine implements SignEngine
```
**`generateKeypair(byteSource?): SignKeypair`**
| Parameter | Type | Description |
|--------------|-------------------------|-----------------------------------------------------------------|
| `byteSource` | `ByteSource \| undefined` | Optional random byte source. When omitted, uses `ed25519.randomPrivateKey()`. |
Returns `SignKeypair { publicKey: Uint8Array; secretKey: Uint8Array }` (both 32 bytes).
**`sign(secretKey, message): Uint8Array`**
Signs `message` directly (Ed25519 is a pure signature scheme — no pre-hashing). Returns a 64-byte signature.
**`verify(publicKey, signature, message): boolean`**
Returns `true` if the signature is valid for the message and public key. Never throws on an invalid signature — returns `false` instead.
---
### `NobleEcdsaP256Engine`
```typescript
class NobleEcdsaP256Engine implements SignEngine
```
**`generateKeypair(byteSource?): SignKeypair`**
| Parameter | Type | Description |
|--------------|-------------------------|------------------------------------------------------------------------------|
| `byteSource` | `ByteSource \| undefined` | Optional byte source. When omitted, `p256.randomPrivateKey()` handles rejection sampling. When supplied, the 32 bytes are used directly and must form a valid P-256 scalar. |
Returns `SignKeypair { publicKey: Uint8Array; secretKey: Uint8Array }`. The public key is 33-byte compressed form.
**`sign(secretKey, message): Uint8Array`**
Hashes `message` with SHA-256, then signs the digest. Returns a compact 64-byte signature (r‖s, without DER encoding).
**`verify(publicKey, signature, message): boolean`**
Hashes `message` with SHA-256 and verifies the compact signature. Returns `true` on success, `false` on any verification failure.
## Implementation Notes
Ed25519 and P-256 differ in their key-space properties. Ed25519 uses a 32-byte seed that is internally hashed to produce the actual scalar, so every seed value is valid. P-256 requires the private scalar to be in the range `[1, n-1]` where `n` is the curve order, which excludes a negligibly small fraction of 32-byte values. The `p256.randomPrivateKey()` function handles rejection-sampling internally; however, when a `byteSource` is injected (for testing), the engine passes the bytes directly without rejection-sampling, placing the validity constraint on the caller.
ECDSA-P256 signs over the SHA-256 digest of the message. This pre-hashing is performed inside the engine rather than in the facade so that all code paths — including direct engine use — apply the hash consistently. The `@noble/curves` P-256 `sign` function also accepts a `lowS` option (enforced by default in newer versions) to produce canonical low-S signatures, which improves compatibility with verification libraries that reject high-S signatures.

View File

@@ -0,0 +1,7 @@
# @suaveplan/crypto-curve
## 0.0.0
- Initial scaffold under `crypto-primitive-replaceability` Phase 0.
- secp256k1, ed25519, x25519, p256 primitives with per-primitive selector layout.
- Noble-backed impls (`@noble/curves`); native stubs throw `PrimitiveNotImplemented`.

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,52 @@
# crypto-curve error codes
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`codes.ts` registers the stable error-code namespace for `@suaveplan/crypto-curve` with `defineCodes` from `@suaveplan/error`. Every error thrown by this package routes through one of these codes so consumers can write stable code-based dispatch and so observability backends see a finite tag set.
The codes are namespaced under `CRYPTO.CURVE` to keep them disjoint from sibling crypto packages (`CRYPTO.CIPHER.*`, `CRYPTO.HASH.*`, `CRYPTO.KEYS.*`).
## Features
- `CRYPTO_CURVE_CODES` — frozen registry mapping symbolic names to wire codes.
- `CryptoCurveCode` — string union of all wire codes for type-safe dispatch.
- Adding a code requires editing this file and re-running typecheck — there is no runtime path that injects a code.
## Usage: Basic Example
```ts
import { CRYPTO_CURVE_CODES, CryptoCurveError } from "@suaveplan/crypto-curve";
throw new CryptoCurveError(
CRYPTO_CURVE_CODES.RECOVERY_BYTE_MISSING,
"error.crypto.curve.recovery-byte-missing",
{ context: { signatureLength: 64 } }
);
```
## Usage: Advanced Example
```ts
import { CRYPTO_CURVE_CODES, type CryptoCurveCode } from "@suaveplan/crypto-curve";
function isRecoverableError(code: CryptoCurveCode): boolean {
return (
code === CRYPTO_CURVE_CODES.RECOVERY_BYTE_MISSING ||
code === CRYPTO_CURVE_CODES.RECOVERY_BYTE_INVALID
);
}
```
## API Reference
- `CRYPTO_CURVE_CODES.RECOVERY_BYTE_MISSING: "CRYPTO.CURVE.RECOVERY_BYTE_MISSING"` — thrown when `recoverPublicKey` receives a 64-byte signature without a recovery byte.
- `CRYPTO_CURVE_CODES.RECOVERY_BYTE_INVALID: "CRYPTO.CURVE.RECOVERY_BYTE_INVALID"` — thrown when the recovery byte is not in `{0, 1}` (some libraries also emit `2` and `3` for compressed-form recovery, which are explicitly rejected here).
- `CRYPTO_CURVE_CODES.RECOVERED_KEY_MISMATCH: "CRYPTO.CURVE.RECOVERED_KEY_MISMATCH"` — reserved for callers that need to surface the comparison-failure error rather than receiving `false` from `verifyWithRecovery`. Today the public API does not throw this code (it returns `false` instead) but it is registered for future use.
- `CryptoCurveCode` — type alias of the union of all values above.
## Implementation Notes
The codes are **stable wire identifiers** — once shipped they cannot be renamed or removed. Adding a new code is permitted; rename-or-delete requires a new spec change with a deprecation cycle. The codes use `SCREAMING_SNAKE_CASE` to match the suaveplan-wide convention; they include the package prefix to disambiguate when multiple packages route through a shared OTel exporter or log aggregator.

View File

@@ -0,0 +1,63 @@
# CryptoCurveError
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`crypto-curve-error.ts` defines the `CryptoCurveError` class used by every error path in `@suaveplan/crypto-curve`. It is a thin alias of `SuaveplanError<CryptoCurveCode>` from `@suaveplan/error` — this means it inherits the full error substrate (cause-chain traversal, OTel span events, structured-logger redaction, i18n message-key resolution, and the wire-format JSON / binary serialiser) without any per-package re-implementation.
Routing every thrown error through this single class is what allows downstream consumers to write `instanceof CryptoCurveError` for package-level dispatch and `error.code === CRYPTO_CURVE_CODES.X` for code-level dispatch, simultaneously, with full type narrowing.
## Features
- Extends `SuaveplanError<CryptoCurveCode>` so the `code` field is typed as a finite union, not an arbitrary string.
- Carries a `cause` chain — the secp256k1 ECDSA recovery path wraps `@noble/curves` errors as `cause` so the original message is preserved in logs.
- Auto-instruments OTel span events when an active span exists (no caller setup required).
- Participates in the wire-format error serialiser (Zod-validated JSON envelope) so errors round-trip across process boundaries cleanly.
## Usage: Basic Example
```ts
import { CRYPTO_CURVE_CODES, CryptoCurveError } from "@suaveplan/crypto-curve";
throw new CryptoCurveError(
CRYPTO_CURVE_CODES.RECOVERY_BYTE_INVALID,
"error.crypto.curve.recovery-byte-invalid",
{ context: { recoveryByte: 7 } }
);
```
## Usage: Advanced Example
```ts
import { CryptoCurveError } from "@suaveplan/crypto-curve";
try {
pubKey = recoverPublicKey(sig, digest);
} catch (err) {
if (err instanceof CryptoCurveError) {
switch (err.code) {
case "CRYPTO.CURVE.RECOVERY_BYTE_MISSING":
throw new Error("Old wallet format — please re-sign with --recovery flag");
case "CRYPTO.CURVE.RECOVERY_BYTE_INVALID":
throw new Error("Signature corrupt or wrong format");
default:
throw err;
}
}
throw err;
}
```
## API Reference
- `CryptoCurveError extends SuaveplanError<CryptoCurveCode>` — public class, exported from the package root.
- `new CryptoCurveError(code, messageKey, options?)` — standard constructor mirroring `SuaveplanError`.
- `error.code: CryptoCurveCode` — typed code field.
- `error.context: Record<string, unknown> | undefined` — structured context attached at throw site.
- `error.cause: unknown` — cause chain.
## Implementation Notes
The constructor does no work beyond delegating to the base class — its only job is to set the generic parameter to `CryptoCurveCode` so consumers see the right type. The error class is the entire reason a separate `CRYPTO_CURVE_CODES` registry exists; without it, callers would either lose code-narrowing or have to import the registry separately. The pattern matches the sibling packages (`CryptoCipherError`, `CryptoHashError`) so all three are interchangeable from a structural standpoint, differing only in which code namespace they pin.

View File

@@ -0,0 +1,65 @@
# Ed25519
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
- [ed25519.select](./ed25519.select/ed25519.select.md) — <!-- begin:auto-docs --> _(no submodules)_- [ed25519.native](./impl/ed25519.native/ed25519.native.md) — <!-- begin:auto-docs --> _(no submodules)_ - [ed25519.native](./impl/ed25519.native/ed25519.native.md) — <!-- begin:auto-docs --> _(no submodules)_- [ed25519.noble](./impl/ed25519.noble/ed25519.noble.md) — <!-- begin:auto-docs --> _(no submodules)_ - [ed25519.noble](./impl/ed25519.noble/ed25519.noble.md) — <!-- begin:auto-docs --> _(no submodules)_- [ed25519](./ed25519.md) — <!-- begin:auto-docs --> _(no submodules)_- ed25519.select — <!-- begin:auto-docs --> _(no submo…
- [ed25519.select](./ed25519.select/ed25519.select.md) — <!-- begin:auto-docs --> _(no submodules)_- [ed25519.native](./impl/ed25519.native/ed25519.native.md) — <!-- begin:auto-docs --> _(no submodules)_- [ed25519.native](./impl/ed25519.native/ed25519.native.md) — <!-- begin:auto-docs --> _(no submodules)_- [ed25519.noble](./impl/ed25519.noble/ed25519.noble.md) — <!-- begin:auto-docs --> _(no submodules)_- [ed25519.noble](./impl/ed25519.noble/ed25519.noble.md) — <!-- begin:auto-docs --> _(no submodules)_- [ed25519](./ed25519.md) — <!-- begin:auto-docs --> _(no submodules)_- [ed25519.select](./ed25519.select/ed25519.select.md) — <!-- begin:auto-docs --> _(no submodules)_- [ed25519.native](./impl/ed25519.native/ed25519.native.md) — <!-- begin:auto-docs --> _(no submodules)_- [ed25519.native](./impl/ed25519.native/ed25519.native.md) — <!-- begin:auto-docs --> _(no submodules)_- [ed25519.noble](./impl/ed25519.noble/ed25519.noble.md) — <!-- begin:auto-docs --> _(no submodules)_- [ed25519.noble](./impl/ed25519.noble/ed25519.noble.md) — <!-- begin:auto-docs --> _(no submodules)_- [ed25519](./ed25519.md) — <!-- begin:auto-docs --> _(no submodules)_- ed25519.select — <!-- begin:…
- [ed25519.select](./ed25519.select/ed25519.select.md) — <!-- begin:auto-docs --> _(no submodules)_- [ed25519.native](./impl/ed25519.native/ed25519.native.md) — <!-- begin:auto-docs --> _(no submodules)_- [ed25519.native](./impl/ed25519.native/ed25519.native.md) — <!-- begin:auto-docs --> _(no submodules)_- [ed25519.noble](./impl/ed25519.noble/ed25519.noble.md) — <!-- begin:auto-docs --> _(no submodules)_- [ed25519.noble](./impl/ed25519.noble/ed25519.noble.md) — <!-- begin:auto-docs --> _(no submodules)_- [ed25519](./ed25519.md) — <!-- begin:auto-docs --> _(no submodules)_- ed25519.select — <!-- begin:auto-docs --> _(no subm…
- [ed25519.select](./ed25519.select/ed25519.select.md) — <!-- begin:auto-docs --> _(no submodules)_- [ed25519.native](./impl/ed25519.native/ed25519.native.md) — <!-- begin:auto-docs --> _(no submodules)_ - [ed25519.native](./impl/ed25519.native/ed25519.native.md) — <!-- begin:auto-docs --> _(no submodules)_- [ed25519.noble](./impl/ed25519.noble/ed25519.noble.md) — <!-- begin:auto-docs --> _(no submodules)_ - [ed25519.noble](./impl/ed25519.noble/ed25519.noble.md) — <!-- begin:auto-docs --> _(no submodules)_- [ed25519](./ed25519.md) — <!-- begin:auto-docs --> _(no submodules)_
## Purpose
Ed25519 is the Edwards-curve Digital Signature Algorithm defined in RFC 8032, the recommended signature scheme for new protocols. Compared with ECDSA it has deterministic signatures (no per-signature randomness needed), constant-time verification of well-formed inputs, small 32-byte public keys and 64-byte signatures, and formal soundness properties that make it the baseline choice for TLS, SSH, JWT signing, and peer-to-peer identity. This module exposes the primitive directly — key generation, signing, verification, and public-key derivation — as thin wrappers over `@noble/curves/ed25519`, with a paired `native` stub that throws `PrimitiveNotImplemented`. Selection between the two is controlled by `crypto.config.ts` at build time.
## Features
- `getPublicKey(privateKey)` → 32-byte compressed Edwards public key.
- `sign(message, privateKey)` → 64-byte deterministic signature.
- `verify(signature, message, publicKey)` → `boolean` that never throws for malformed inputs.
- `randomPrivateKey()` → 32 random bytes (every 32-byte value is a valid Ed25519 secret; no retry loop needed).
- RFC 8032 §7.1 vector 1 is covered in tests.
- Native stub throws `PrimitiveNotImplemented('ed25519')` — flip to `'native'` only after per-primitive audit (Phase 3).
## Usage: Basic Example
```ts
import * as ed25519 from "@suaveplan/crypto-curve/ed25519";
const sk = ed25519.randomPrivateKey();
const pk = ed25519.getPublicKey(sk);
const msg = new TextEncoder().encode("hello");
const sig = ed25519.sign(msg, sk);
console.log(ed25519.verify(sig, msg, pk)); // true
```
## Usage: Advanced Example
```ts
import * as ed25519 from "@suaveplan/crypto-curve/ed25519";
import { zeroize } from "@suaveplan/crypto-utils/zeroize";
function signAndDiscardKey(msg: Uint8Array): { pk: Uint8Array; sig: Uint8Array } {
const sk = ed25519.randomPrivateKey();
try {
const pk = ed25519.getPublicKey(sk);
const sig = ed25519.sign(msg, sk);
return { pk, sig };
} finally {
zeroize(sk);
}
}
const { pk, sig } = signAndDiscardKey(new Uint8Array([1, 2, 3]));
console.log(pk.length, sig.length);
```
## API Reference
- `getPublicKey(privateKey: Uint8Array): Uint8Array` — 32-byte public key. Throws if `privateKey.length !== 32`.
- `sign(message: Uint8Array, privateKey: Uint8Array): Uint8Array` — 64-byte signature.
- `verify(signature: Uint8Array, message: Uint8Array, publicKey: Uint8Array): boolean` — returns `false` instead of throwing on malformed inputs.
- `randomPrivateKey(): Uint8Array` — 32 random bytes via `@suaveplan/crypto-utils/random`.
## Implementation Notes
Noble's `verify` throws on some malformed inputs (bad-length public key, non-canonical signature encodings) rather than returning `false`. The `noble` impl wraps the call in a try/catch so every flavour exposes a boolean-only contract — CI's differential fuzz harness depends on that uniformity. Signing is deterministic, so the same `(msg, sk)` pair always produces the same signature: this is a deliberate property that makes replay/caching sound and prevents fault-attack reuse. For signing data larger than a few megabytes, streaming APIs are not offered here on purpose — callers should compose over the hash-then-sign idiom instead.

View File

@@ -0,0 +1,57 @@
# ed25519.select
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`ed25519.select.ts` is the build-time primitive selector for the Ed25519 implementation in `@suaveplan/crypto-curve`. It is generated by `bun scripts/gen-crypto-selects.ts` and contains a single `export *` that points to either `ed25519.noble.ts` or `ed25519.native.ts` based on the `ed25519` entry in `crypto.config.ts`.
Callers import through `ed25519.ts` (the stable public entry point), which delegates here. The selector indirection allows switching backends by regenerating one file without changing any caller imports. Currently the Noble backend is selected because the native backend throws `PrimitiveNotImplemented`.
## Features
- Single-file re-export of `getPublicKey`, `sign`, `verify`, and `randomPrivateKey`.
- Generated by `bun scripts/gen-crypto-selects.ts` — never hand-edited.
- Controlled by `crypto.config.ts`: set `ed25519` to `'noble'` or `'native'`.
- Enables static dead-code elimination of the inactive backend.
- Maintains a stable API across backend switches.
## Usage: Basic Example
```ts
// Import via the stable public entry point.
import * as ed25519 from "@suaveplan/crypto-curve/ed25519";
const sk = ed25519.randomPrivateKey();
const pk = ed25519.getPublicKey(sk);
const sig = ed25519.sign(new TextEncoder().encode("hello"), sk);
console.log(ed25519.verify(sig, new TextEncoder().encode("hello"), pk)); // true
```
## Usage: Advanced Example
```ts
import * as ed25519 from "@suaveplan/crypto-curve/ed25519";
import { toHex } from "@suaveplan/crypto-hash/encoding";
// Generate a key pair and publish the hex-encoded public key.
function generateKeyPair(): { secretKey: Uint8Array; publicKeyHex: string } {
const secretKey = ed25519.randomPrivateKey();
const pk = ed25519.getPublicKey(secretKey);
return { secretKey, publicKeyHex: toHex(pk) };
}
```
## API Reference
Re-exports from the active implementation:
- `getPublicKey(privateKey: Uint8Array): Uint8Array` — 32-byte public key.
- `sign(message: Uint8Array, privateKey: Uint8Array): Uint8Array` — 64-byte deterministic signature.
- `verify(signature: Uint8Array, message: Uint8Array, publicKey: Uint8Array): boolean` — never throws.
- `randomPrivateKey(): Uint8Array` — 32 random bytes.
## Implementation Notes
Unlike secp256k1 or P-256, every 32-byte sequence is a valid Ed25519 private key, so `randomPrivateKey` needs no rejection sampling. The selector file is marked `/* GENERATED — DO NOT EDIT */`; hand-editing it will be overwritten by the next generator run. The generator is idempotent.

View File

@@ -0,0 +1,64 @@
# ed25519.native
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`ed25519.native.ts` is the native-backend placeholder for the Ed25519 primitive in `@suaveplan/crypto-curve`. It exports the same four functions as `ed25519.noble.ts` — `getPublicKey`, `sign`, `verify`, and `randomPrivateKey` — each throwing `PrimitiveNotImplemented("ed25519")`.
This stub maintains the two-implementation layout for Ed25519. It must be present so the selector generator can point to it without structural errors. When an in-house or platform-native Ed25519 implementation is ready, this file is updated and the selector regenerated.
## Features
- Satisfies `getPublicKey`, `sign`, `verify`, and `randomPrivateKey` interfaces.
- Throws `PrimitiveNotImplemented("ed25519")` from every function.
- Zero runtime dependencies beyond `@suaveplan/crypto-utils/errors`.
- Clearly communicates that the native path is not yet active.
## Usage: Basic Example
```ts
// Active only when crypto.config.ts sets ed25519 to 'native'.
import * as ed25519 from "@suaveplan/crypto-curve/ed25519";
try {
ed25519.randomPrivateKey();
} catch (err) {
// PrimitiveNotImplemented: ed25519
console.error(err);
}
```
## Usage: Advanced Example
```ts
// Detect availability before use in a capability-adaptive application.
import * as ed25519 from "@suaveplan/crypto-curve/ed25519";
function isEd25519Available(): boolean {
try {
ed25519.randomPrivateKey();
return true;
} catch {
return false;
}
}
```
## API Reference
All functions throw `PrimitiveNotImplemented("ed25519")`:
- `getPublicKey(_privateKey: Uint8Array): Uint8Array`
- `sign(_message: Uint8Array, _privateKey: Uint8Array): Uint8Array`
- `verify(_signature: Uint8Array, _message: Uint8Array, _publicKey: Uint8Array): boolean`
- `randomPrivateKey(): Uint8Array`
## Implementation Notes
Underscore-prefixed parameters suppress unused-variable diagnostics without lint-disable comments. When the native implementation is ready (RFC 8032 KAT suite must pass), parameters are renamed, `throw` replaced with the real algorithm, and the selector regenerated. The `PrimitiveNotImplemented` error participates in the full `@suaveplan/error` structured-error system.
### Status
Placeholder native stub — throws `PrimitiveNotImplemented` until a Node `crypto.sign("ed25519", ...)` bridge lands for the `native` engine. Callers compile against the `ed25519.select.ts` selector which falls through to the `@noble/curves` implementation in the `noble` engine slot, so shipping this stub does not break runtime behaviour on any platform today.

View File

@@ -0,0 +1,83 @@
# ed25519.noble
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`ed25519.noble.ts` is the Noble-backed concrete implementation of the Ed25519 primitive in `@suaveplan/crypto-curve`. It wraps the Ed25519 operations from `@noble/curves/ed25519`, providing public-key derivation, deterministic signing, signature verification, and random private-key generation.
Ed25519 (RFC 8032 §5.1) is the Edwards-curve Digital Signature Algorithm on Curve25519. It is the recommended signature scheme for new protocols due to its deterministic signatures (no per-signature randomness), small key and signature sizes (32 and 64 bytes respectively), and formal soundness properties. This file is the production implementation; `ed25519.native.ts` is the placeholder stub.
## Features
- `getPublicKey(privateKey)` — 32-byte compressed Edwards public key via scalar multiplication.
- `sign(message, privateKey)` — 64-byte deterministic signature over the raw message (Noble handles internal hashing per RFC 8032).
- `verify(signature, message, publicKey)` — boolean; wraps Noble's throwing `verify` in try/catch.
- `randomPrivateKey()` — 32 random bytes from `@suaveplan/crypto-utils/random`.
## Usage: Basic Example
```ts
import * as ed25519 from "@suaveplan/crypto-curve/ed25519";
const sk = ed25519.randomPrivateKey();
const pk = ed25519.getPublicKey(sk);
const msg = new TextEncoder().encode("authenticate");
const sig = ed25519.sign(msg, sk);
console.log(ed25519.verify(sig, msg, pk)); // true
```
## Usage: Advanced Example
```ts
import * as ed25519 from "@suaveplan/crypto-curve/ed25519";
import { zeroize } from "@suaveplan/crypto-utils/zeroize";
// Sign a JWT payload and zeroize the private key immediately.
function signPayload(payload: string): { pk: Uint8Array; sig: Uint8Array } {
const sk = ed25519.randomPrivateKey();
const encoded = new TextEncoder().encode(payload);
try {
return {
pk: ed25519.getPublicKey(sk),
sig: ed25519.sign(encoded, sk),
};
} finally {
zeroize(sk);
}
}
```
## API Reference
### `getPublicKey(privateKey: Uint8Array): Uint8Array`
| Parameter | Type | Description |
|-----------|------|-------------|
| `privateKey` | `Uint8Array` | Any 32-byte value. |
**Returns:** 32-byte compressed Edwards-form public key.
### `sign(message: Uint8Array, privateKey: Uint8Array): Uint8Array`
Signs the raw message (Noble internally hashes with SHA-512 per RFC 8032).
| Parameter | Type | Description |
|-----------|------|-------------|
| `message` | `Uint8Array` | Raw message bytes (any length). |
| `privateKey` | `Uint8Array` | 32-byte private key. |
**Returns:** 64-byte signature.
### `verify(signature: Uint8Array, message: Uint8Array, publicKey: Uint8Array): boolean`
Returns `false` for malformed inputs rather than throwing.
### `randomPrivateKey(): Uint8Array`
Returns 32 random bytes. No rejection sampling needed (all 32-byte values are valid).
## Implementation Notes
Ed25519 signs the raw message, not a pre-hash, because the RFC 8032 construction hashes internally using SHA-512 for the nonce and the final signature computation. This differs from secp256k1/P-256, which expect a pre-hashed digest. Noble's `verify` throws on some malformed inputs; the `try/catch` wrapper normalises this to a boolean, consistent with the rest of the curve implementations. RFC 8032 §7.1 test vector 1 passes in the test suite.

View File

@@ -0,0 +1,73 @@
# p256.native
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`p256.native.ts` is the native-backend placeholder for the NIST P-256 primitive in `@suaveplan/crypto-curve`. It exports the complete P-256 surface — `getPublicKey`, `sign`, `verify`, and `randomPrivateKey` — each throwing `PrimitiveNotImplemented("p256")`.
P-256 has native WebCrypto support in all modern browsers (`SubtleCrypto.sign` with `{ name: "ECDSA", hash: "SHA-256" }`) and Node.js 15+. A future native implementation would delegate to these platform APIs, enabling hardware-backed key operations. The stub keeps the layout complete until that work lands.
## Features
- Satisfies the full P-256 interface: `getPublicKey`, `sign`, `verify`, `randomPrivateKey`.
- Throws `PrimitiveNotImplemented("p256")` from every function.
- Zero runtime dependencies beyond `@suaveplan/crypto-utils/errors`.
## Usage: Basic Example
```ts
// Active only when crypto.config.ts sets p256 to 'native'.
import * as p256 from "@suaveplan/crypto-curve/p256";
try {
p256.randomPrivateKey();
} catch (err) {
// PrimitiveNotImplemented: p256
console.error(err);
}
```
## Usage: Advanced Example
```ts
// Capability detection for native P-256 availability.
import * as p256 from "@suaveplan/crypto-curve/p256";
async function nativeP256Available(): Promise<boolean> {
try {
p256.randomPrivateKey();
return true;
} catch {
// Fall back to WebCrypto directly.
try {
await crypto.subtle.generateKey(
{ name: "ECDSA", namedCurve: "P-256" },
false,
["sign"]
);
return true;
} catch {
return false;
}
}
}
```
## API Reference
All functions throw `PrimitiveNotImplemented("p256")`:
- `getPublicKey(_privateKey: Uint8Array): Uint8Array`
- `sign(_digest: Uint8Array, _privateKey: Uint8Array): Uint8Array`
- `verify(_signature: Uint8Array, _digest: Uint8Array, _publicKey: Uint8Array): boolean`
- `randomPrivateKey(): Uint8Array`
## Implementation Notes
The native P-256 backend is particularly valuable for browser environments, where `SubtleCrypto` can use hardware security keys (YubiKey, TPM) via the non-extractable key pattern. A native implementation would wrap the async `SubtleCrypto` APIs and handle the DER-to-compact signature conversion that WebCrypto uses by default. Until that implementation is ready and has passed FIPS 186-4 CAVP vectors, this stub surfaces a clear error. Underscore-prefixed parameters suppress unused-variable warnings.
### Status
Placeholder native stub — throws `PrimitiveNotImplemented` until a WebCrypto `ECDSA P-256` bridge is wired through. Today the `p256.select` selector falls through to the Noble implementation in every engine mode; that path is covered by the NIST CAVP KAT harness in `@suaveplan/crypto-utils`, so the production path is fully exercised.

View File

@@ -0,0 +1,83 @@
# p256.noble
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`p256.noble.ts` is the Noble-backed concrete implementation of the NIST P-256 primitive in `@suaveplan/crypto-curve`. It wraps the `p256` object from `@noble/curves/p256`, providing ECDSA signing and verification, public-key derivation, and rejection-sampled private-key generation.
P-256 (secp256r1, FIPS 186-4) is the most widely deployed NIST curve. Its operations — including ECDSA with SHA-256 (ES256) — are hardware-accelerated in browser WebCrypto and available natively across all major TLS implementations. Signatures are returned in compact 64-byte (r ‖ s) form. Callers must pre-hash the message; this module operates on digests.
## Features
- `getPublicKey(privateKey)` — 33-byte compressed P-256 public key.
- `sign(digest, privateKey)` — deterministic ECDSA (RFC 6979); compact 64-byte output.
- `verify(signature, digest, publicKey)` — boolean; wraps Noble's throwing `verify`.
- `randomPrivateKey()` — rejection-sampled 32-byte key via `@suaveplan/crypto-utils/random`.
- Noble's `p256.utils.isValidPrivateKey` gates the rejection loop.
## Usage: Basic Example
```ts
import * as p256 from "@suaveplan/crypto-curve/p256";
import { sha256 } from "@suaveplan/crypto-hash/hash";
const sk = p256.randomPrivateKey();
const pk = p256.getPublicKey(sk);
const digest = sha256(new TextEncoder().encode("payload"));
const sig = p256.sign(digest, sk);
console.log(p256.verify(sig, digest, pk)); // true
```
## Usage: Advanced Example
```ts
import * as p256 from "@suaveplan/crypto-curve/p256";
import { sha256 } from "@suaveplan/crypto-hash/hash";
import { zeroize } from "@suaveplan/crypto-utils/zeroize";
// Create a one-time signing key pair for a request.
function ephemeralSign(data: Uint8Array): { pk: Uint8Array; sig: Uint8Array } {
const sk = p256.randomPrivateKey();
try {
return {
pk: p256.getPublicKey(sk),
sig: p256.sign(sha256(data), sk),
};
} finally {
zeroize(sk);
}
}
```
## API Reference
### `getPublicKey(privateKey: Uint8Array): Uint8Array`
| Parameter | Type | Description |
|-----------|------|-------------|
| `privateKey` | `Uint8Array` | 32-byte scalar in [1, n−1]. |
**Returns:** 33-byte compressed public key.
### `sign(digest: Uint8Array, privateKey: Uint8Array): Uint8Array`
| Parameter | Type | Description |
|-----------|------|-------------|
| `digest` | `Uint8Array` | Pre-computed message hash (32 bytes for SHA-256/ES256). |
| `privateKey` | `Uint8Array` | 32-byte private key. |
**Returns:** 64-byte compact (r ‖ s) ECDSA signature.
### `verify(signature: Uint8Array, digest: Uint8Array, publicKey: Uint8Array): boolean`
Returns `false` for malformed inputs rather than throwing.
### `randomPrivateKey(): Uint8Array`
Returns a valid P-256 scalar. Retries until `isValidPrivateKey` passes.
## Implementation Notes
`sign` calls Noble's `.toBytes("compact")` to produce the flat 64-byte format; callers needing ASN.1 DER must convert externally. `verify` wraps Noble's call in try/catch because some malformed input combinations (non-canonical signatures, out-of-range coordinates) cause exceptions rather than `false` returns. The rejection loop in `randomPrivateKey` is statistically negligible — probability of an invalid scalar is approximately 1 in 2¹²⁸. NIST FIPS 186-4 / CAVP test vectors pass in the test suite.

View File

@@ -0,0 +1,64 @@
# p256
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
- [p256.native](./impl/p256.native/p256.native.md) — <!-- begin:auto-docs --> _(no submodules)_- [p256.noble](./impl/p256.noble/p256.noble.md) — <!-- begin:auto-docs --> _(no submodules)_ - [p256.noble](./impl/p256.noble/p256.noble.md) — <!-- begin:auto-docs --> _(no submodules)_- [p256.select](./p256.select/p256.select.md) — <!-- begin:auto-docs --> _(no submodules)_ - [p256.select](./p256.select/p256.select.md) — <!-- begin:auto-docs --> _(no submodules)_- [p256](./p256.md) — <!-- begin:auto-docs --> _(no submodules)_- p256.native — <!-- begin:auto-docs --> _(no submodul…
- [p256.native](./impl/p256.native/p256.native.md) — <!-- begin:auto-docs --> _(no submodules)_- [p256.noble](./impl/p256.noble/p256.noble.md) — <!-- begin:auto-docs --> _(no submodules)_- [p256.noble](./impl/p256.noble/p256.noble.md) — <!-- begin:auto-docs --> _(no submodules)_- [p256.select](./p256.select/p256.select.md) — <!-- begin:auto-docs --> _(no submodules)_- [p256.select](./p256.select/p256.select.md) — <!-- begin:auto-docs --> _(no submodules)_- [p256](./p256.md) — <!-- begin:auto-docs --> _(no submodules)_- [p256.native](./impl/p256.native/p256.native.md) — <!-- begin:auto-docs --> _(no submodules)_- [p256.noble](./impl/p256.noble/p256.noble.md) — <!-- begin:auto-docs --> _(no submodules)_- [p256.noble](./impl/p256.noble/p256.noble.md) — <!-- begin:auto-docs --> _(no submodules)_- [p256.select](./p256.select/p256.select.md) — <!-- begin:auto-docs --> _(no submodules)_- [p256.select](./p256.select/p256.select.md) — <!-- begin:auto-docs --> _(no submodules)_- [p256](./p256.md) — <!-- begin:auto-docs --> _(no submodules)_- p256.native — <!-- begin:aut…
- [p256.native](./impl/p256.native/p256.native.md) — <!-- begin:auto-docs --> _(no submodules)_- [p256.noble](./impl/p256.noble/p256.noble.md) — <!-- begin:auto-docs --> _(no submodules)_- [p256.noble](./impl/p256.noble/p256.noble.md) — <!-- begin:auto-docs --> _(no submodules)_- [p256.select](./p256.select/p256.select.md) — <!-- begin:auto-docs --> _(no submodules)_- [p256.select](./p256.select/p256.select.md) — <!-- begin:auto-docs --> _(no submodules)_- [p256](./p256.md) — <!-- begin:auto-docs --> _(no submodules)_- p256.native — <!-- begin:auto-docs --> _(no submodu…
- [p256.native](./impl/p256.native/p256.native.md) — <!-- begin:auto-docs --> _(no submodules)_- [p256.noble](./impl/p256.noble/p256.noble.md) — <!-- begin:auto-docs --> _(no submodules)_ - [p256.noble](./impl/p256.noble/p256.noble.md) — <!-- begin:auto-docs --> _(no submodules)_- [p256.select](./p256.select/p256.select.md) — <!-- begin:auto-docs --> _(no submodules)_ - [p256.select](./p256.select/p256.select.md) — <!-- begin:auto-docs --> _(no submodules)_- [p256](./p256.md) — <!-- begin:auto-docs --> _(no submodules)_
## Purpose
`p256.ts` is the stable public entry point for the NIST P-256 elliptic-curve primitive in `@suaveplan/crypto-curve`. It re-exports everything from `p256.select.ts`, which forwards to either the Noble-backed or native implementation based on `crypto.config.ts`.
P-256 (also known as secp256r1 or prime256v1, standardised in FIPS 186-4) is the most widely deployed NIST curve. It is mandatory in TLS 1.3, used in ECDSA-P256 JWT signing (ES256), WebAuthn, and is natively available in every browser's WebCrypto API. Callers import from this file and remain insulated from implementation changes.
## Features
- `getPublicKey(privateKey)` — 33-byte compressed public key.
- `sign(digest, privateKey)` — compact 64-byte ECDSA signature (r ‖ s). Caller pre-hashes.
- `verify(signature, digest, publicKey)` — boolean; never throws on malformed inputs.
- `randomPrivateKey()` — rejection-sampled 32-byte private key.
## Usage: Basic Example
```ts
import * as p256 from "@suaveplan/crypto-curve/p256";
import { sha256 } from "@suaveplan/crypto-hash/hash";
const sk = p256.randomPrivateKey();
const pk = p256.getPublicKey(sk);
const msg = new TextEncoder().encode("hello P-256");
const sig = p256.sign(sha256(msg), sk);
console.log(p256.verify(sig, sha256(msg), pk)); // true
```
## Usage: Advanced Example
```ts
import * as p256 from "@suaveplan/crypto-curve/p256";
import { sha256 } from "@suaveplan/crypto-hash/hash";
import { toHex } from "@suaveplan/crypto-hash/encoding";
// Sign a JSON payload and produce a JWS-style compact token.
function signPayload(payload: object, sk: Uint8Array): string {
const header = btoa(JSON.stringify({ alg: "ES256" }));
const body = btoa(JSON.stringify(payload));
const sigInput = new TextEncoder().encode(`${header}.${body}`);
const sig = p256.sign(sha256(sigInput), sk);
return `${header}.${body}.${toHex(sig)}`;
}
```
## API Reference
All symbols re-exported from `p256.select.ts`:
- `getPublicKey(privateKey: Uint8Array): Uint8Array`
- `sign(digest: Uint8Array, privateKey: Uint8Array): Uint8Array`
- `verify(signature: Uint8Array, digest: Uint8Array, publicKey: Uint8Array): boolean`
- `randomPrivateKey(): Uint8Array`
## Implementation Notes
This file is a zero-logic re-export barrel. Its sole purpose is the stable import path `@suaveplan/crypto-curve/p256`. P-256 callers must pre-hash their messages before calling `sign`; unlike Ed25519, P-256's ECDSA does not hash internally. Use SHA-256 for ES256 compatibility.

View File

@@ -0,0 +1,59 @@
# p256.select
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`p256.select.ts` is the build-time primitive selector for NIST P-256 in `@suaveplan/crypto-curve`. Generated by `bun scripts/gen-crypto-selects.ts`, it re-exports one concrete backend — `p256.noble.ts` or `p256.native.ts` — according to the `p256` field in `crypto.config.ts`.
Callers import through the stable `p256.ts` entry point. This selector level insulates all call sites from backend changes: regenerating this file is the only step required to switch implementations. Currently the Noble backend is active because the native stub throws `PrimitiveNotImplemented`.
## Features
- Single re-export of `getPublicKey`, `sign`, `verify`, and `randomPrivateKey`.
- Generated — never hand-edited.
- Controlled by `crypto.config.ts`: set `p256` to `'noble'` or `'native'`.
- Enables dead-code elimination of the inactive backend.
- Maintains a stable API across backend switches.
## Usage: Basic Example
```ts
import * as p256 from "@suaveplan/crypto-curve/p256";
const sk = p256.randomPrivateKey();
const pk = p256.getPublicKey(sk);
console.log(pk.length); // 33
```
## Usage: Advanced Example
```ts
import * as p256 from "@suaveplan/crypto-curve/p256";
import { sha256 } from "@suaveplan/crypto-hash/hash";
// Sign and verify in one call.
function roundTrip(message: Uint8Array): boolean {
const sk = p256.randomPrivateKey();
const pk = p256.getPublicKey(sk);
const digest = sha256(message);
const sig = p256.sign(digest, sk);
return p256.verify(sig, digest, pk);
}
console.log(roundTrip(new TextEncoder().encode("test"))); // true
```
## API Reference
Re-exports from the active implementation:
- `getPublicKey(privateKey: Uint8Array): Uint8Array` — 33-byte compressed P-256 public key.
- `sign(digest: Uint8Array, privateKey: Uint8Array): Uint8Array` — 64-byte compact ECDSA signature.
- `verify(signature: Uint8Array, digest: Uint8Array, publicKey: Uint8Array): boolean`
- `randomPrivateKey(): Uint8Array` — rejection-sampled 32-byte key.
## Implementation Notes
The selector file contains a single `export * from` line and is marked `/* GENERATED — DO NOT EDIT */`. The generator is idempotent; regenerating does not alter the file if the configured backend has not changed. Tree-shaking of the inactive backend happens at bundle time — both impl files are present in source but only the selected one is reachable from this export.

View File

@@ -0,0 +1,88 @@
# p384.native
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`p384.native.ts` is the native-backend placeholder for the NIST P-384
primitive in `@suaveplan/crypto-curve`. It exports the complete P-384
surface — `getPublicKey`, `sign`, `verify`, and `randomPrivateKey` — each
throwing `PrimitiveNotImplemented("p384")`.
P-384 has native WebCrypto support in all modern browsers
(`SubtleCrypto.sign` with `{ name: "ECDSA", hash: "SHA-384" }`) and
Node.js 15+. A future native implementation would delegate to these
platform APIs, enabling hardware-backed key operations. The stub keeps
the layout complete until that work lands.
## Features
- Satisfies the full P-384 interface: `getPublicKey`, `sign`, `verify`,
`randomPrivateKey`.
- Throws `PrimitiveNotImplemented("p384")` from every function.
- Zero runtime dependencies beyond `@suaveplan/crypto-utils/errors`.
## Usage: Basic Example
```ts
// Active only when crypto.config.ts sets p384 to 'native'.
import * as p384 from "@suaveplan/crypto-curve/p384";
try {
p384.randomPrivateKey();
} catch (err) {
// PrimitiveNotImplemented: p384
console.error(err);
}
```
## Usage: Advanced Example
```ts
import * as p384 from "@suaveplan/crypto-curve/p384";
async function nativeP384Available(): Promise<boolean> {
try {
p384.randomPrivateKey();
return true;
} catch {
try {
await crypto.subtle.generateKey(
{ name: "ECDSA", namedCurve: "P-384" },
false,
["sign"]
);
return true;
} catch {
return false;
}
}
}
```
## API Reference
All functions throw `PrimitiveNotImplemented("p384")`:
- `getPublicKey(_privateKey: Uint8Array): Uint8Array`
- `sign(_digest: Uint8Array, _privateKey: Uint8Array): Uint8Array`
- `verify(_signature: Uint8Array, _digest: Uint8Array, _publicKey: Uint8Array): boolean`
- `randomPrivateKey(): Uint8Array`
## Implementation Notes
The native P-384 backend is particularly valuable for browser
environments, where `SubtleCrypto` can use hardware security keys via the
non-extractable key pattern. A native implementation would wrap the async
`SubtleCrypto` APIs and handle the DER-to-compact signature conversion
that WebCrypto uses by default. Until that implementation is ready and
has passed FIPS 186-4 CAVP vectors, this stub surfaces a clear error.
### Status
Placeholder native stub — throws `PrimitiveNotImplemented` until a
WebCrypto `ECDSA P-384` bridge is wired through. Today the
`p384.select` selector falls through to the Noble implementation in
every engine mode; the production path is exercised by the noble
roundtrip suite in `p384.test.ts`.

View File

@@ -0,0 +1,92 @@
# p384.noble
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`p384.noble.ts` is the Noble-backed concrete implementation of the NIST
P-384 primitive in `@suaveplan/crypto-curve`. It wraps the `p384` object
from `@noble/curves/p384`, providing ECDSA signing and verification,
public-key derivation, and rejection-sampled private-key generation.
P-384 (secp384r1, FIPS 186-4) is the higher-security NIST curve used for
COSE `ES384` signatures, Apple WebAuthn attestation in some
configurations, FIDO2 device certificates that opt out of P-256 for
additional security margin, and US Government Suite B deployments
requiring ≥192-bit security strength. Signatures are returned in compact
96-byte (r ‖ s) form. Callers must pre-hash the message; this module
operates on digests.
## Features
- `getPublicKey(privateKey)` — 49-byte compressed P-384 public key.
- `sign(digest, privateKey)` — deterministic ECDSA (RFC 6979); compact
96-byte output.
- `verify(signature, digest, publicKey)` — boolean; wraps Noble's
throwing `verify`.
- `randomPrivateKey()` — rejection-sampled 48-byte key via
`@suaveplan/crypto-utils/random`.
- Noble's `p384.utils.isValidPrivateKey` gates the rejection loop.
## Usage: Basic Example
```ts
import * as p384 from "@suaveplan/crypto-curve/p384";
import { sha384 } from "@suaveplan/crypto-hash";
const sk = p384.randomPrivateKey();
const pk = p384.getPublicKey(sk);
const digest = sha384(new TextEncoder().encode("payload"));
const sig = p384.sign(digest, sk);
console.log(p384.verify(sig, digest, pk)); // true
```
## Usage: Advanced Example
```ts
import * as p384 from "@suaveplan/crypto-curve/p384";
import { sha384 } from "@suaveplan/crypto-hash";
import { zeroize } from "@suaveplan/crypto-utils";
function ephemeralSign(data: Uint8Array): { pk: Uint8Array; sig: Uint8Array } {
const sk = p384.randomPrivateKey();
try {
return {
pk: p384.getPublicKey(sk),
sig: p384.sign(sha384(data), sk),
};
} finally {
zeroize(sk);
}
}
```
## API Reference
### `getPublicKey(privateKey: Uint8Array): Uint8Array`
48-byte scalar → 49-byte compressed public key.
### `sign(digest: Uint8Array, privateKey: Uint8Array): Uint8Array`
Pre-hashed input (48 bytes for SHA-384) → 96-byte compact (r ‖ s) signature.
### `verify(signature: Uint8Array, digest: Uint8Array, publicKey: Uint8Array): boolean`
Returns `false` for malformed inputs rather than throwing.
### `randomPrivateKey(): Uint8Array`
Returns a valid P-384 scalar. Retries until `isValidPrivateKey` passes.
## Implementation Notes
`sign` calls Noble's `.toBytes("compact")` to produce the flat 96-byte
format; callers needing ASN.1 DER must convert externally. `verify` wraps
Noble's call in try/catch because some malformed input combinations
(non-canonical signatures, out-of-range coordinates) cause exceptions
rather than `false` returns. The rejection loop in `randomPrivateKey` is
statistically negligible — the curve order is approximately `2^384` so
the rejection rate is essentially zero. NIST FIPS 186-4 / CAVP P-384
test vectors pass through the noble underlying implementation.

View File

@@ -0,0 +1,81 @@
# p384
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
- [p384.native](./impl/p384.native/p384.native.md) — <!-- begin:auto-docs --> _(no submodules)_- [p384.noble](./impl/p384.noble/p384.noble.md) — <!-- begin:auto-docs --> _(no submodules)_ - [p384.noble](./impl/p384.noble/p384.noble.md) — <!-- begin:auto-docs --> _(no submodules)_- [p384.select](./p384.select/p384.select.md) — <!-- begin:auto-docs --> _(no submodules)_ - [p384.select](./p384.select/p384.select.md) — <!-- begin:auto-docs --> _(no submodules)_- [p384](./p384.md) — <!-- begin:auto-docs --> _(no submodules)_- p384.native — <!-- begin:auto-docs --> _(no submodul…
- [p384.native](./impl/p384.native/p384.native.md) — <!-- begin:auto-docs --> _(no submodules)_- [p384.noble](./impl/p384.noble/p384.noble.md) — <!-- begin:auto-docs --> _(no submodules)_- [p384.noble](./impl/p384.noble/p384.noble.md) — <!-- begin:auto-docs --> _(no submodules)_- [p384.select](./p384.select/p384.select.md) — <!-- begin:auto-docs --> _(no submodules)_- [p384.select](./p384.select/p384.select.md) — <!-- begin:auto-docs --> _(no submodules)_- [p384](./p384.md) — <!-- begin:auto-docs --> _(no submodules)_- [p384.native](./impl/p384.native/p384.native.md) — <!-- begin:auto-docs --> _(no submodules)_- [p384.noble](./impl/p384.noble/p384.noble.md) — <!-- begin:auto-docs --> _(no submodules)_- [p384.noble](./impl/p384.noble/p384.noble.md) — <!-- begin:auto-docs --> _(no submodules)_- [p384.select](./p384.select/p384.select.md) — <!-- begin:auto-docs --> _(no submodules)_- [p384.select](./p384.select/p384.select.md) — <!-- begin:auto-docs --> _(no submodules)_- [p384](./p384.md) — <!-- begin:auto-docs --> _(no submodules)_- p384.native — <!-- begin:aut…
- [p384.native](./impl/p384.native/p384.native.md) — <!-- begin:auto-docs --> _(no submodules)_- [p384.noble](./impl/p384.noble/p384.noble.md) — <!-- begin:auto-docs --> _(no submodules)_- [p384.noble](./impl/p384.noble/p384.noble.md) — <!-- begin:auto-docs --> _(no submodules)_- [p384.select](./p384.select/p384.select.md) — <!-- begin:auto-docs --> _(no submodules)_- [p384.select](./p384.select/p384.select.md) — <!-- begin:auto-docs --> _(no submodules)_- [p384](./p384.md) — <!-- begin:auto-docs --> _(no submodules)_- p384.native — <!-- begin:auto-docs --> _(no submodu…
- [p384.native](./impl/p384.native/p384.native.md) — <!-- begin:auto-docs --> _(no submodules)_- [p384.noble](./impl/p384.noble/p384.noble.md) — <!-- begin:auto-docs --> _(no submodules)_ - [p384.noble](./impl/p384.noble/p384.noble.md) — <!-- begin:auto-docs --> _(no submodules)_- [p384.select](./p384.select/p384.select.md) — <!-- begin:auto-docs --> _(no submodules)_ - [p384.select](./p384.select/p384.select.md) — <!-- begin:auto-docs --> _(no submodules)_- [p384](./p384.md) — <!-- begin:auto-docs --> _(no submodules)_
## Purpose
`p384.ts` is the stable public entry point for the NIST P-384 elliptic-curve
primitive in `@suaveplan/crypto-curve`. It re-exports everything from
`p384.select.ts`, which forwards to either the Noble-backed or native
implementation based on `crypto.config.ts`.
P-384 (also known as secp384r1, standardised in FIPS 186-4) is the higher-
security NIST curve used for COSE `ES384` signatures (RFC 9053 §2.1), Apple
WebAuthn attestation in some configurations, FIDO2 device certificates that
opt out of P-256 for additional security margin, and US Government Suite B
deployments that require ≥192-bit security strength. The curve has a 384-bit
order so private keys are 48 bytes, compressed public keys are 49 bytes, and
compact ECDSA signatures are 96 bytes.
## Features
- `getPublicKey(privateKey)` — 49-byte compressed public key.
- `sign(digest, privateKey)` — compact 96-byte ECDSA signature (r ‖ s).
Caller pre-hashes (SHA-384 for ES384 wire compatibility).
- `verify(signature, digest, publicKey)` — boolean; never throws on
malformed inputs.
- `randomPrivateKey()` — rejection-sampled 48-byte private key.
## Usage: Basic Example
```ts
import * as p384 from "@suaveplan/crypto-curve/p384";
import { sha384 } from "@suaveplan/crypto-hash";
const sk = p384.randomPrivateKey();
const pk = p384.getPublicKey(sk);
const msg = new TextEncoder().encode("hello P-384");
const sig = p384.sign(sha384(msg), sk);
console.log(p384.verify(sig, sha384(msg), pk)); // true
```
## Usage: Advanced Example
```ts
import * as p384 from "@suaveplan/crypto-curve/p384";
import { sha384 } from "@suaveplan/crypto-hash";
// COSE_Sign1 verify path for an ES384-signed message.
function verifyCoseSign1Es384(
tbs: Uint8Array,
signature: Uint8Array,
publicKey: Uint8Array,
): boolean {
return p384.verify(signature, sha384(tbs), publicKey);
}
```
## API Reference
All symbols re-exported from `p384.select.ts`:
- `getPublicKey(privateKey: Uint8Array): Uint8Array`
- `sign(digest: Uint8Array, privateKey: Uint8Array): Uint8Array`
- `verify(signature: Uint8Array, digest: Uint8Array, publicKey: Uint8Array): boolean`
- `randomPrivateKey(): Uint8Array`
## Implementation Notes
This file is a zero-logic re-export barrel. Its sole purpose is the stable
import path `@suaveplan/crypto-curve/p384`. Callers MUST pre-hash with
SHA-384 (or another 384-bit digest) before calling `sign` — ECDSA does not
hash internally. Use SHA-384 specifically for COSE ES384 / JWA ES384 wire
compatibility. The `randomPrivateKey` rejection loop terminates after a
constant expected number of iterations because nearly the full 48-byte
space is a valid scalar (the curve order is `2^384 - 2^128 - …` so the
rejection rate is negligible).

View File

@@ -0,0 +1,70 @@
# p384.select
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`p384.select.ts` is the build-time primitive selector for NIST P-384 in
`@suaveplan/crypto-curve`. Generated by `bun scripts/gen-crypto-selects.ts`,
it re-exports one concrete backend — `p384.noble.ts` or `p384.native.ts` —
according to the `p384` field in `crypto.config.ts`.
Callers import through the stable `p384.ts` entry point. This selector level
insulates all call sites from backend changes: regenerating this file is the
only step required to switch implementations. Currently the Noble backend is
active because the native stub throws `PrimitiveNotImplemented`.
## Features
- Single re-export of `getPublicKey`, `sign`, `verify`, and `randomPrivateKey`.
- Generated — never hand-edited.
- Controlled by `crypto.config.ts`: set `p384` to `'noble'` or `'native'`.
- Enables dead-code elimination of the inactive backend.
- Maintains a stable API across backend switches.
## Usage: Basic Example
```ts
import * as p384 from "@suaveplan/crypto-curve/p384";
const sk = p384.randomPrivateKey();
const pk = p384.getPublicKey(sk);
console.log(pk.length); // 49
```
## Usage: Advanced Example
```ts
import * as p384 from "@suaveplan/crypto-curve/p384";
import { sha384 } from "@suaveplan/crypto-hash";
function roundTrip(message: Uint8Array): boolean {
const sk = p384.randomPrivateKey();
const pk = p384.getPublicKey(sk);
const digest = sha384(message);
const sig = p384.sign(digest, sk);
return p384.verify(sig, digest, pk);
}
```
## API Reference
Re-exports from the active implementation:
- `getPublicKey(privateKey: Uint8Array): Uint8Array` — 49-byte compressed
P-384 public key.
- `sign(digest: Uint8Array, privateKey: Uint8Array): Uint8Array` — 96-byte
compact ECDSA signature.
- `verify(signature: Uint8Array, digest: Uint8Array, publicKey: Uint8Array): boolean`
- `randomPrivateKey(): Uint8Array` — rejection-sampled 48-byte key.
## Implementation Notes
The selector file contains a single `export * from` line and is marked
`/* GENERATED — DO NOT EDIT */`. The generator is idempotent; regenerating
does not alter the file if the configured backend has not changed.
Tree-shaking of the inactive backend happens at bundle time — both impl
files are present in source but only the selected one is reachable from
this export. Adding a new backend (e.g. a WebCrypto path) is a one-line
edit to the generator plus the new impl file.

View File

@@ -0,0 +1,89 @@
# p521.native
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`p521.native.ts` is the native-backend placeholder for the NIST P-521
primitive in `@suaveplan/crypto-curve`. It exports the complete P-521
surface — `getPublicKey`, `sign`, `verify`, and `randomPrivateKey` —
each throwing `PrimitiveNotImplemented("p521")`.
P-521 has native WebCrypto support in all modern browsers
(`SubtleCrypto.sign` with `{ name: "ECDSA", hash: "SHA-512" }`) and
Node.js 15+. A future native implementation would delegate to these
platform APIs, enabling hardware-backed key operations. The stub keeps
the layout complete until that work lands.
## Features
- Satisfies the full P-521 interface: `getPublicKey`, `sign`, `verify`,
`randomPrivateKey`.
- Throws `PrimitiveNotImplemented("p521")` from every function.
- Zero runtime dependencies beyond `@suaveplan/crypto-utils/errors`.
## Usage: Basic Example
```ts
// Active only when crypto.config.ts sets p521 to 'native'.
import * as p521 from "@suaveplan/crypto-curve/p521";
try {
p521.randomPrivateKey();
} catch (err) {
// PrimitiveNotImplemented: p521
console.error(err);
}
```
## Usage: Advanced Example
```ts
import * as p521 from "@suaveplan/crypto-curve/p521";
async function nativeP521Available(): Promise<boolean> {
try {
p521.randomPrivateKey();
return true;
} catch {
try {
await crypto.subtle.generateKey(
{ name: "ECDSA", namedCurve: "P-521" },
false,
["sign"]
);
return true;
} catch {
return false;
}
}
}
```
## API Reference
All functions throw `PrimitiveNotImplemented("p521")`:
- `getPublicKey(_privateKey: Uint8Array): Uint8Array`
- `sign(_digest: Uint8Array, _privateKey: Uint8Array): Uint8Array`
- `verify(_signature: Uint8Array, _digest: Uint8Array, _publicKey: Uint8Array): boolean`
- `randomPrivateKey(): Uint8Array`
## Implementation Notes
P-521 native support varies across platforms — Safari historically lagged
behind Chrome / Firefox in WebCrypto P-521 support and capabilities
detection is essential for production rollouts. A native implementation
would handle the byte-width quirks (66-byte coordinates left-padded to
67-byte compressed pubkeys) that WebCrypto already encodes. Until that
implementation is ready and has passed FIPS 186-4 CAVP vectors, this
stub surfaces a clear error.
### Status
Placeholder native stub — throws `PrimitiveNotImplemented` until a
WebCrypto `ECDSA P-521` bridge is wired through. Today the
`p521.select` selector falls through to the Noble implementation in
every engine mode; the production path is exercised by the noble
roundtrip suite in `p521.test.ts`.

View File

@@ -0,0 +1,92 @@
# p521.noble
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`p521.noble.ts` is the Noble-backed concrete implementation of the NIST
P-521 primitive in `@suaveplan/crypto-curve`. It wraps the `p521` object
from `@noble/curves/p521`, providing ECDSA signing and verification,
public-key derivation, and rejection-sampled private-key generation.
P-521 (secp521r1, FIPS 186-4) is the highest-security NIST curve and is
required by COSE `ES512` (RFC 9053 §2.1), some FIPS-mode deployments, and
high-assurance long-lived identity certificates. Signatures are returned
in compact 132-byte (r ‖ s) form, where r and s are each 66 bytes (the
521-bit scalar left-padded). Callers must pre-hash the message; this
module operates on digests.
## Features
- `getPublicKey(privateKey)` — 67-byte compressed P-521 public key.
- `sign(digest, privateKey)` — deterministic ECDSA (RFC 6979); compact
132-byte output.
- `verify(signature, digest, publicKey)` — boolean; wraps Noble's
throwing `verify`.
- `randomPrivateKey()` — rejection-sampled 66-byte key via
`@suaveplan/crypto-utils/random`.
- Noble's `p521.utils.isValidPrivateKey` gates the rejection loop.
## Usage: Basic Example
```ts
import * as p521 from "@suaveplan/crypto-curve/p521";
import { sha512 } from "@suaveplan/crypto-hash";
const sk = p521.randomPrivateKey();
const pk = p521.getPublicKey(sk);
const digest = sha512(new TextEncoder().encode("payload"));
const sig = p521.sign(digest, sk);
console.log(p521.verify(sig, digest, pk)); // true
```
## Usage: Advanced Example
```ts
import * as p521 from "@suaveplan/crypto-curve/p521";
import { sha512 } from "@suaveplan/crypto-hash";
import { zeroize } from "@suaveplan/crypto-utils";
function ephemeralSign(data: Uint8Array): { pk: Uint8Array; sig: Uint8Array } {
const sk = p521.randomPrivateKey();
try {
return {
pk: p521.getPublicKey(sk),
sig: p521.sign(sha512(data), sk),
};
} finally {
zeroize(sk);
}
}
```
## API Reference
### `getPublicKey(privateKey: Uint8Array): Uint8Array`
66-byte scalar → 67-byte compressed public key.
### `sign(digest: Uint8Array, privateKey: Uint8Array): Uint8Array`
Pre-hashed input (64 bytes for SHA-512) → 132-byte compact (r ‖ s)
signature.
### `verify(signature: Uint8Array, digest: Uint8Array, publicKey: Uint8Array): boolean`
Returns `false` for malformed inputs rather than throwing.
### `randomPrivateKey(): Uint8Array`
Returns a valid P-521 scalar. Retries until `isValidPrivateKey` passes.
## Implementation Notes
`sign` calls Noble's `.toBytes("compact")` to produce the flat 132-byte
format. The 66-byte width comes from rounding 521 bits up to the next
byte boundary; the high byte of every scalar / coordinate has only its
low 9 bits populated for the maximum value. `verify` wraps Noble's call
in try/catch because some malformed input combinations cause exceptions
rather than `false` returns. The rejection rate for `randomPrivateKey`
is approximately `1 − 2^(-7)` because only the low 9 bits of the high
byte matter; the loop typically converges in one or two iterations.

View File

@@ -0,0 +1,80 @@
# p521
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
- [p521.native](./impl/p521.native/p521.native.md) — <!-- begin:auto-docs --> _(no submodules)_- [p521.noble](./impl/p521.noble/p521.noble.md) — <!-- begin:auto-docs --> _(no submodules)_ - [p521.noble](./impl/p521.noble/p521.noble.md) — <!-- begin:auto-docs --> _(no submodules)_- [p521.select](./p521.select/p521.select.md) — <!-- begin:auto-docs --> _(no submodules)_ - [p521.select](./p521.select/p521.select.md) — <!-- begin:auto-docs --> _(no submodules)_- [p521](./p521.md) — <!-- begin:auto-docs --> _(no submodules)_- p521.native — <!-- begin:auto-docs --> _(no submodul…
- [p521.native](./impl/p521.native/p521.native.md) — <!-- begin:auto-docs --> _(no submodules)_- [p521.noble](./impl/p521.noble/p521.noble.md) — <!-- begin:auto-docs --> _(no submodules)_- [p521.noble](./impl/p521.noble/p521.noble.md) — <!-- begin:auto-docs --> _(no submodules)_- [p521.select](./p521.select/p521.select.md) — <!-- begin:auto-docs --> _(no submodules)_- [p521.select](./p521.select/p521.select.md) — <!-- begin:auto-docs --> _(no submodules)_- [p521](./p521.md) — <!-- begin:auto-docs --> _(no submodules)_- [p521.native](./impl/p521.native/p521.native.md) — <!-- begin:auto-docs --> _(no submodules)_- [p521.noble](./impl/p521.noble/p521.noble.md) — <!-- begin:auto-docs --> _(no submodules)_- [p521.noble](./impl/p521.noble/p521.noble.md) — <!-- begin:auto-docs --> _(no submodules)_- [p521.select](./p521.select/p521.select.md) — <!-- begin:auto-docs --> _(no submodules)_- [p521.select](./p521.select/p521.select.md) — <!-- begin:auto-docs --> _(no submodules)_- [p521](./p521.md) — <!-- begin:auto-docs --> _(no submodules)_- p521.native — <!-- begin:aut…
- [p521.native](./impl/p521.native/p521.native.md) — <!-- begin:auto-docs --> _(no submodules)_- [p521.noble](./impl/p521.noble/p521.noble.md) — <!-- begin:auto-docs --> _(no submodules)_- [p521.noble](./impl/p521.noble/p521.noble.md) — <!-- begin:auto-docs --> _(no submodules)_- [p521.select](./p521.select/p521.select.md) — <!-- begin:auto-docs --> _(no submodules)_- [p521.select](./p521.select/p521.select.md) — <!-- begin:auto-docs --> _(no submodules)_- [p521](./p521.md) — <!-- begin:auto-docs --> _(no submodules)_- p521.native — <!-- begin:auto-docs --> _(no submodu…
- [p521.native](./impl/p521.native/p521.native.md) — <!-- begin:auto-docs --> _(no submodules)_- [p521.noble](./impl/p521.noble/p521.noble.md) — <!-- begin:auto-docs --> _(no submodules)_ - [p521.noble](./impl/p521.noble/p521.noble.md) — <!-- begin:auto-docs --> _(no submodules)_- [p521.select](./p521.select/p521.select.md) — <!-- begin:auto-docs --> _(no submodules)_ - [p521.select](./p521.select/p521.select.md) — <!-- begin:auto-docs --> _(no submodules)_- [p521](./p521.md) — <!-- begin:auto-docs --> _(no submodules)_
## Purpose
`p521.ts` is the stable public entry point for the NIST P-521 elliptic-curve
primitive in `@suaveplan/crypto-curve`. It re-exports everything from
`p521.select.ts`, which forwards to either the Noble-backed or native
implementation based on `crypto.config.ts`.
P-521 (also known as secp521r1, standardised in FIPS 186-4) is the highest-
security NIST curve and is required by COSE `ES512` (RFC 9053 §2.1), some
FIPS-mode deployments, and high-assurance long-lived identity certificates.
The curve has a 521-bit order; private keys and coordinates are
left-padded to 66 bytes so the entire data path stays byte-aligned.
Compact ECDSA signatures are 132 bytes (2 × 66).
## Features
- `getPublicKey(privateKey)` — 67-byte compressed public key.
- `sign(digest, privateKey)` — compact 132-byte ECDSA signature (r ‖ s).
Caller pre-hashes (SHA-512 for ES512 wire compatibility).
- `verify(signature, digest, publicKey)` — boolean; never throws on
malformed inputs.
- `randomPrivateKey()` — rejection-sampled 66-byte private key.
## Usage: Basic Example
```ts
import * as p521 from "@suaveplan/crypto-curve/p521";
import { sha512 } from "@suaveplan/crypto-hash";
const sk = p521.randomPrivateKey();
const pk = p521.getPublicKey(sk);
const msg = new TextEncoder().encode("hello P-521");
const sig = p521.sign(sha512(msg), sk);
console.log(p521.verify(sig, sha512(msg), pk)); // true
```
## Usage: Advanced Example
```ts
import * as p521 from "@suaveplan/crypto-curve/p521";
import { sha512 } from "@suaveplan/crypto-hash";
// COSE_Sign1 verify path for an ES512-signed message.
function verifyCoseSign1Es512(
tbs: Uint8Array,
signature: Uint8Array,
publicKey: Uint8Array,
): boolean {
return p521.verify(signature, sha512(tbs), publicKey);
}
```
## API Reference
All symbols re-exported from `p521.select.ts`:
- `getPublicKey(privateKey: Uint8Array): Uint8Array`
- `sign(digest: Uint8Array, privateKey: Uint8Array): Uint8Array`
- `verify(signature: Uint8Array, digest: Uint8Array, publicKey: Uint8Array): boolean`
- `randomPrivateKey(): Uint8Array`
## Implementation Notes
This file is a zero-logic re-export barrel. Its sole purpose is the stable
import path `@suaveplan/crypto-curve/p521`. Callers MUST pre-hash with
SHA-512 (or another 512-bit digest) before calling `sign` — ECDSA does not
hash internally. The 66-byte coordinate width comes from the 521-bit curve
order rounded up to the next byte boundary; the high byte of each scalar
is mostly zero (only the low 9 bits are populated for the maximum value).
This matches the COSE wire format which also uses 66-byte left-padded
coordinates for P-521 keys.

View File

@@ -0,0 +1,70 @@
# p521.select
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`p521.select.ts` is the build-time primitive selector for NIST P-521 in
`@suaveplan/crypto-curve`. Generated by `bun scripts/gen-crypto-selects.ts`,
it re-exports one concrete backend — `p521.noble.ts` or `p521.native.ts` —
according to the `p521` field in `crypto.config.ts`.
Callers import through the stable `p521.ts` entry point. This selector
level insulates all call sites from backend changes: regenerating this
file is the only step required to switch implementations. Currently the
Noble backend is active because the native stub throws
`PrimitiveNotImplemented`.
## Features
- Single re-export of `getPublicKey`, `sign`, `verify`, and `randomPrivateKey`.
- Generated — never hand-edited.
- Controlled by `crypto.config.ts`: set `p521` to `'noble'` or `'native'`.
- Enables dead-code elimination of the inactive backend.
- Maintains a stable API across backend switches.
## Usage: Basic Example
```ts
import * as p521 from "@suaveplan/crypto-curve/p521";
const sk = p521.randomPrivateKey();
const pk = p521.getPublicKey(sk);
console.log(pk.length); // 67
```
## Usage: Advanced Example
```ts
import * as p521 from "@suaveplan/crypto-curve/p521";
import { sha512 } from "@suaveplan/crypto-hash";
function roundTrip(message: Uint8Array): boolean {
const sk = p521.randomPrivateKey();
const pk = p521.getPublicKey(sk);
const digest = sha512(message);
const sig = p521.sign(digest, sk);
return p521.verify(sig, digest, pk);
}
```
## API Reference
Re-exports from the active implementation:
- `getPublicKey(privateKey: Uint8Array): Uint8Array` — 67-byte compressed
P-521 public key.
- `sign(digest: Uint8Array, privateKey: Uint8Array): Uint8Array` — 132-byte
compact ECDSA signature.
- `verify(signature: Uint8Array, digest: Uint8Array, publicKey: Uint8Array): boolean`
- `randomPrivateKey(): Uint8Array` — rejection-sampled 66-byte key.
## Implementation Notes
The selector file contains a single `export * from` line and is marked
`/* GENERATED — DO NOT EDIT */`. P-521 keys carry an additional invariant
that callers need to be aware of: the high byte of every scalar / coordinate
must have the top 7 bits zero (the curve has a 521-bit order, byte-aligned
to 66 bytes). The noble backend enforces this in `isValidPrivateKey`; the
rejection loop in `randomPrivateKey` retries until the constraint holds.

View File

@@ -0,0 +1,65 @@
# secp256k1.native
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`secp256k1.native.ts` is the native-backend placeholder for the secp256k1 primitive in `@suaveplan/crypto-curve`. It exports the complete secp256k1 surface — `getPublicKey`, `sign`, `verify`, `getSharedSecret`, and `randomPrivateKey` — each throwing `PrimitiveNotImplemented("secp256k1")`.
This stub keeps the two-implementation selector layout structurally complete. When the in-house secp256k1 implementation is ready and has passed the required test vectors, this file is replaced with the real code and the selector is regenerated to point here.
## Features
- Satisfies the full secp256k1 interface: `getPublicKey`, `sign`, `verify`, `getSharedSecret`, `randomPrivateKey`.
- Throws `PrimitiveNotImplemented("secp256k1")` from every function.
- Zero runtime dependencies beyond `@suaveplan/crypto-utils/errors`.
- Provides clear, typed errors identifying which primitive is unimplemented.
## Usage: Basic Example
```ts
// Active only when crypto.config.ts sets secp256k1 to 'native'.
import * as secp256k1 from "@suaveplan/crypto-curve/secp256k1";
try {
secp256k1.randomPrivateKey();
} catch (err) {
// PrimitiveNotImplemented: secp256k1
console.error(err);
}
```
## Usage: Advanced Example
```ts
// Capability detection — safe to call when backend may not be implemented.
import * as secp256k1 from "@suaveplan/crypto-curve/secp256k1";
function isSecp256k1Available(): boolean {
try {
secp256k1.randomPrivateKey();
return true;
} catch {
return false;
}
}
```
## API Reference
All functions throw `PrimitiveNotImplemented("secp256k1")`:
- `getPublicKey(_privateKey: Uint8Array): Uint8Array`
- `sign(_digest: Uint8Array, _privateKey: Uint8Array): Uint8Array`
- `verify(_signature: Uint8Array, _digest: Uint8Array, _publicKey: Uint8Array): boolean`
- `getSharedSecret(_privateKey: Uint8Array, _peerPublicKey: Uint8Array): Uint8Array`
- `randomPrivateKey(): Uint8Array`
## Implementation Notes
Each function has underscore-prefixed parameters to suppress unused-variable diagnostics without inline suppressions. `PrimitiveNotImplemented` from `@suaveplan/crypto-utils/errors` extends `SuaveplanError` and participates in the full error serialisation, cause-chain traversal, and OTel span-event system. When the native implementation lands, parameter prefixes are removed, the `throw` replaced, and the selector regenerated — no changes are required outside this file.
### Status
Placeholder native stub — throws `PrimitiveNotImplemented` until a libsecp256k1 binding is added to the `native` engine slot. Until then, `secp256k1.select` routes every call through `@noble/curves/secp256k1`, which is constant-time by construction and differentially fuzzed against `@noble/secp256k1` via the cross-impl harness in `@suaveplan/crypto-utils`.

View File

@@ -0,0 +1,91 @@
# secp256k1.noble
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`secp256k1.noble.ts` is the Noble-backed concrete implementation of the secp256k1 primitive in `@suaveplan/crypto-curve`. It wraps the secp256k1 operations from `@noble/curves/secp256k1`, providing public-key derivation, ECDSA signing and verification, ECDH shared-secret computation, and random private-key generation.
secp256k1 is the domain parameter set used by Bitcoin, Ethereum, and a wide range of blockchain and Web3 protocols. Private keys are 32-byte scalars; public keys are 33-byte compressed points; signatures are compact 64-byte (r ‖ s) form. Callers are expected to pre-hash their messages before signing — this module operates on digests.
## Features
- `getPublicKey(privateKey)` — 33-byte compressed public key via point multiplication.
- `sign(digest, privateKey)` — deterministic ECDSA signature (RFC 6979); compact 64-byte output.
- `verify(signature, digest, publicKey)` — boolean result; catches Noble exceptions for malformed inputs.
- `getSharedSecret(privateKey, peerPublicKey)` — ECDH shared secret for key agreement.
- `randomPrivateKey()` — rejection-sampled 32-byte key; retries until `isValidPrivateKey` passes.
## Usage: Basic Example
```ts
import * as secp256k1 from "@suaveplan/crypto-curve/secp256k1";
import { sha256 } from "@suaveplan/crypto-hash/hash";
const sk = secp256k1.randomPrivateKey();
const pk = secp256k1.getPublicKey(sk);
const msg = new TextEncoder().encode("hello secp256k1");
const sig = secp256k1.sign(sha256(msg), sk);
console.log(secp256k1.verify(sig, sha256(msg), pk)); // true
```
## Usage: Advanced Example
```ts
import * as secp256k1 from "@suaveplan/crypto-curve/secp256k1";
import { hkdf } from "@suaveplan/crypto-hash/kdf";
// ECDH + HKDF to derive an encryption key.
function deriveSharedKey(
localSk: Uint8Array,
remotePk: Uint8Array,
context: string
): Uint8Array {
const raw = secp256k1.getSharedSecret(localSk, remotePk);
return hkdf("SHA-256", raw, {
salt: new Uint8Array(32),
info: new TextEncoder().encode(context),
length: 32,
});
}
```
## API Reference
### `getPublicKey(privateKey: Uint8Array): Uint8Array`
Derives the 33-byte compressed public key for `privateKey`.
| Parameter | Type | Description |
|-----------|------|-------------|
| `privateKey` | `Uint8Array` | 32-byte scalar in [1, n−1]. |
**Returns:** 33-byte compressed public key.
### `sign(digest: Uint8Array, privateKey: Uint8Array): Uint8Array`
Signs a pre-hashed digest. Uses RFC 6979 deterministic nonce generation.
| Parameter | Type | Description |
|-----------|------|-------------|
| `digest` | `Uint8Array` | 32-byte hash of the message. |
| `privateKey` | `Uint8Array` | 32-byte private key. |
**Returns:** 64-byte compact (r ‖ s) signature.
### `verify(signature: Uint8Array, digest: Uint8Array, publicKey: Uint8Array): boolean`
Verifies a signature. Returns `false` for malformed inputs instead of throwing.
### `getSharedSecret(privateKey: Uint8Array, peerPublicKey: Uint8Array): Uint8Array`
Computes the ECDH shared secret. The result must be passed through a KDF before use as a symmetric key.
### `randomPrivateKey(): Uint8Array`
Returns a uniformly random 32-byte valid private key. Uses rejection sampling.
## Implementation Notes
`verify` wraps the Noble call in try/catch because Noble throws for some malformed input combinations (non-canonical encodings, out-of-range coordinates). The wrapper converts all exceptions to `false`, maintaining a uniform boolean contract. `randomPrivateKey` uses rejection sampling because not all 32-byte sequences are valid secp256k1 scalars — the probability of rejection is negligible (~1 in 2¹²⁸) so the loop almost never iterates more than once. `sign` uses Noble's `toBytes("compact")` to get the flat 64-byte form rather than DER; callers that need DER encoding must convert externally.

View File

@@ -0,0 +1,88 @@
# secp256k1
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
- [secp256k1.native](./impl/secp256k1.native/secp256k1.native.md) — <!-- begin:auto-docs --> _(no submodules)_- [secp256k1.noble](./impl/secp256k1.noble/secp256k1.noble.md) — <!-- begin:auto-docs --> _(no submodules)_ - [secp256k1.noble](./impl/secp256k1.noble/secp256k1.noble.md) — <!-- begin:auto-docs --> _(no submodules)_- [secp256k1.select](./secp256k1.select/secp256k1.select.md) — <!-- begin:auto-docs --> _(no submodules)_ - [secp256k1.select](./secp256k1.select/secp256k1.select.md) — <!-- begin:auto-docs --> _(no submodules)_- [secp256k1](./secp256k1.md) — <!-- begin:auto-docs --> _(no submodules)_- secp256k1.native — <!-- begin:auto-docs --> _(no sub…
- [secp256k1.native](./impl/secp256k1.native/secp256k1.native.md) — <!-- begin:auto-docs --> _(no submodules)_- [secp256k1.noble](./impl/secp256k1.noble/secp256k1.noble.md) — <!-- begin:auto-docs --> _(no submodules)_- [secp256k1.noble](./impl/secp256k1.noble/secp256k1.noble.md) — <!-- begin:auto-docs --> _(no submodules)_- [secp256k1.select](./secp256k1.select/secp256k1.select.md) — <!-- begin:auto-docs --> _(no submodules)_- [secp256k1.select](./secp256k1.select/secp256k1.select.md) — <!-- begin:auto-docs --> _(no submodules)_- [secp256k1](./secp256k1.md) — <!-- begin:auto-docs --> _(no submodules)_- [secp256k1.native](./impl/secp256k1.native/secp256k1.native.md) — <!-- begin:auto-docs --> _(no submodules)_- [secp256k1.noble](./impl/secp256k1.noble/secp256k1.noble.md) — <!-- begin:auto-docs --> _(no submodules)_- [secp256k1.noble](./impl/secp256k1.noble/secp256k1.noble.md) — <!-- begin:auto-docs --> _(no submodules)_- [secp256k1.select](./secp256k1.select/secp256k1.select.md) — <!-- begin:auto-docs --> _(no submodules)_- [secp256k1.select](./secp256k1.select/secp256k1.select.md) — <!-- begin:auto-docs --> _(no submodules)_- [secp256k1](./secp256k1.md) — <!-- begin:auto-docs --> _(no submodules)_- secp256k1.native — <!-- begi…
- [secp256k1.native](./impl/secp256k1.native/secp256k1.native.md) — <!-- begin:auto-docs --> _(no submodules)_- [secp256k1.noble](./impl/secp256k1.noble/secp256k1.noble.md) — <!-- begin:auto-docs --> _(no submodules)_- [secp256k1.noble](./impl/secp256k1.noble/secp256k1.noble.md) — <!-- begin:auto-docs --> _(no submodules)_- [secp256k1.select](./secp256k1.select/secp256k1.select.md) — <!-- begin:auto-docs --> _(no submodules)_- [secp256k1.select](./secp256k1.select/secp256k1.select.md) — <!-- begin:auto-docs --> _(no submodules)_- [secp256k1](./secp256k1.md) — <!-- begin:auto-docs --> _(no submodules)_- secp256k1.native — <!-- begin:auto-docs --> _(no su…
- [secp256k1.native](./impl/secp256k1.native/secp256k1.native.md) — <!-- begin:auto-docs --> _(no submodules)_- [secp256k1.noble](./impl/secp256k1.noble/secp256k1.noble.md) — <!-- begin:auto-docs --> _(no submodules)_ - [secp256k1.noble](./impl/secp256k1.noble/secp256k1.noble.md) — <!-- begin:auto-docs --> _(no submodules)_- [secp256k1.select](./secp256k1.select/secp256k1.select.md) — <!-- begin:auto-docs --> _(no submodules)_ - [secp256k1.select](./secp256k1.select/secp256k1.select.md) — <!-- begin:auto-docs --> _(no submodules)_- [secp256k1](./secp256k1.md) — <!-- begin:auto-docs --> _(no submodules)_
## Purpose
`secp256k1.ts` is the stable public entry point for the secp256k1 elliptic-curve primitive in `@suaveplan/crypto-curve`. It re-exports everything from the generated selector file `secp256k1.select.ts`, which in turn forwards to either the Noble-backed or native implementation depending on `crypto.config.ts`.
secp256k1 is the Koblitz curve used by Bitcoin, Ethereum, and many other blockchain protocols. It supports ECDSA signing and ECDH key agreement. Callers import from this file and remain insulated from implementation changes.
## Features
- `getPublicKey(privateKey)` — derives the 33-byte compressed public key.
- `sign(digest, privateKey)` — produces a compact 64-byte ECDSA signature (r ‖ s). Caller must pre-hash the message.
- `verify(signature, digest, publicKey)` — returns `boolean`, never throws on malformed inputs.
- `getSharedSecret(privateKey, peerPublicKey)` — computes ECDH shared secret.
- `randomPrivateKey()` — generates a cryptographically valid 32-byte private key via rejection sampling.
- `signWithRecovery(digest, privateKey)` — produces a 65-byte signature (`64-byte compact || 1-byte recovery`). Recovery byte is `0` or `1`.
- `recoverPublicKey(signatureWithRecovery, digest)` — recovers the 33-byte compressed public key from a 65-byte signature-with-recovery and the original digest. Throws `CryptoCurveError(RECOVERY_BYTE_MISSING)` for 64-byte inputs and `CryptoCurveError(RECOVERY_BYTE_INVALID)` when the recovery byte is not `0` or `1`.
- `verifyWithRecovery(signatureWithRecovery, digest, expectedPublicKey)` — recovers the public key and compares it to `expectedPublicKey` in constant time. Returns `false` on mismatch (does not throw); structural errors (`RECOVERY_BYTE_MISSING`, `RECOVERY_BYTE_INVALID`) still throw.
## Usage: Basic Example
```ts
import * as secp256k1 from "@suaveplan/crypto-curve/secp256k1";
import { sha256 } from "@suaveplan/crypto-hash/hash";
const sk = secp256k1.randomPrivateKey();
const pk = secp256k1.getPublicKey(sk);
const msg = new TextEncoder().encode("transaction data");
const digest = sha256(msg);
const sig = secp256k1.sign(digest, sk);
console.log(secp256k1.verify(sig, digest, pk)); // true
```
## Usage: Advanced Example
```ts
import * as secp256k1 from "@suaveplan/crypto-curve/secp256k1";
import { hkdf } from "@suaveplan/crypto-hash/kdf";
import { zeroize } from "@suaveplan/crypto-utils/zeroize";
// Ephemeral ECDH for a sealed message.
function sealedEphemeralKey(recipientPk: Uint8Array): {
ephemeralPk: Uint8Array;
sharedKey: Uint8Array;
} {
const ephemeralSk = secp256k1.randomPrivateKey();
try {
const ephemeralPk = secp256k1.getPublicKey(ephemeralSk);
const rawSecret = secp256k1.getSharedSecret(ephemeralSk, recipientPk);
const sharedKey = hkdf("SHA-256", rawSecret, {
salt: new Uint8Array(32),
info: new TextEncoder().encode("secp256k1-sealed"),
length: 32,
});
return { ephemeralPk, sharedKey };
} finally {
zeroize(ephemeralSk);
}
}
```
## API Reference
All symbols are re-exported from `secp256k1.select.ts`. See the active implementation (`secp256k1.noble.ts` or `secp256k1.native.ts`) for full parameter documentation.
- `getPublicKey(privateKey: Uint8Array): Uint8Array`
- `sign(digest: Uint8Array, privateKey: Uint8Array): Uint8Array`
- `verify(signature: Uint8Array, digest: Uint8Array, publicKey: Uint8Array): boolean`
- `getSharedSecret(privateKey: Uint8Array, peerPublicKey: Uint8Array): Uint8Array`
- `randomPrivateKey(): Uint8Array`
- `signWithRecovery(digest: Uint8Array, privateKey: Uint8Array): Uint8Array`
- `recoverPublicKey(signatureWithRecovery: Uint8Array, digest: Uint8Array): Uint8Array`
- `verifyWithRecovery(signatureWithRecovery: Uint8Array, digest: Uint8Array, expectedPublicKey: Uint8Array): boolean`
### Signature-with-recovery layout
The 65-byte form is `[r:32 | s:32 | recovery:1]` (big-endian). The recovery byte selects which of the two possible public keys derived from `(r, s, digest)` was the signer; valid values are `0` and `1`. Some libraries (for compressed-vs-uncompressed keys) emit `2` and `3` — those are explicitly rejected here because suaveplan always returns the compressed (33-byte) form, so the lift bit is implicit.
## Implementation Notes
This file is a zero-overhead re-export barrel. Its only role is to provide a stable import path (`@suaveplan/crypto-curve/secp256k1`) that survives future backend changes. The selector file underneath is generated; this file is not. Do not add logic here — it belongs in the concrete implementation files.

View File

@@ -0,0 +1,55 @@
# secp256k1.select
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`secp256k1.select.ts` is the build-time primitive selector for the secp256k1 implementation in `@suaveplan/crypto-curve`. It is generated by `bun scripts/gen-crypto-selects.ts` and re-exports exactly one backend — either `secp256k1.noble.ts` or `secp256k1.native.ts` — based on the `secp256k1` entry in `crypto.config.ts`.
Callers import through the public entry point `secp256k1.ts`, which delegates here. This indirection means switching from the Noble backend to the native backend requires only regenerating this file; all import paths above it remain unchanged.
## Features
- Single-file re-export of all secp256k1 operations (`getPublicKey`, `sign`, `verify`, `getSharedSecret`, `randomPrivateKey`).
- Generated by `bun scripts/gen-crypto-selects.ts` — never hand-edited.
- Controlled by `crypto.config.ts`: set `secp256k1` to `'noble'` or `'native'`.
- Enables static dead-code elimination of the unselected backend.
- Maintains a stable API surface across backend switches.
## Usage: Basic Example
```ts
// Always import through the stable entry point, not this select file.
import * as secp256k1 from "@suaveplan/crypto-curve/secp256k1";
const sk = secp256k1.randomPrivateKey();
const pk = secp256k1.getPublicKey(sk);
console.log(pk.length); // 33 (compressed)
```
## Usage: Advanced Example
```ts
import * as secp256k1 from "@suaveplan/crypto-curve/secp256k1";
import { sha256 } from "@suaveplan/crypto-hash/hash";
// Sign a pre-hashed digest (hash-then-sign pattern).
function signDigest(message: Uint8Array, privateKey: Uint8Array): Uint8Array {
return secp256k1.sign(sha256(message), privateKey);
}
```
## API Reference
Re-exports all symbols from the active implementation. At the Noble backend these are:
- `getPublicKey(privateKey: Uint8Array): Uint8Array`
- `sign(digest: Uint8Array, privateKey: Uint8Array): Uint8Array`
- `verify(signature: Uint8Array, digest: Uint8Array, publicKey: Uint8Array): boolean`
- `getSharedSecret(privateKey: Uint8Array, peerPublicKey: Uint8Array): Uint8Array`
- `randomPrivateKey(): Uint8Array`
## Implementation Notes
This file contains a single `export * from` line and is marked `/* GENERATED — DO NOT EDIT */`. The generator reads `crypto.config.ts`, looks up the backend for `secp256k1`, and writes the appropriate path. Currently the Noble backend is the only production-ready option; the native stub throws `PrimitiveNotImplemented("secp256k1")`. The selector enables tree-shaking of the inactive backend at the bundler level, keeping the deployed bundle minimal.

View File

@@ -0,0 +1,61 @@
# x25519.native
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`x25519.native.ts` is the native-backend placeholder for the X25519 primitive in `@suaveplan/crypto-curve`. It exports `getPublicKey`, `getSharedSecret`, and `randomPrivateKey` — each throwing `PrimitiveNotImplemented("x25519")` — to maintain the two-implementation selector layout.
This stub signals that a native X25519 backend (for example, delegating to the host's WebCrypto `ECDH` with `X25519` or a native module) is planned but not yet active. The selector can be pointed here by changing `crypto.config.ts` and regenerating, at which point callers would get the implementation.
## Features
- Satisfies `getPublicKey`, `getSharedSecret`, and `randomPrivateKey` interfaces.
- Throws `PrimitiveNotImplemented("x25519")` unconditionally from every function.
- Zero runtime dependencies beyond `@suaveplan/crypto-utils/errors`.
## Usage: Basic Example
```ts
// Active only when crypto.config.ts sets x25519 to 'native'.
import * as x25519 from "@suaveplan/crypto-curve/x25519";
try {
x25519.randomPrivateKey();
} catch (err) {
// PrimitiveNotImplemented: x25519
console.error(err);
}
```
## Usage: Advanced Example
```ts
// Graceful degradation pattern.
import * as x25519 from "@suaveplan/crypto-curve/x25519";
function tryGetPublicKey(sk: Uint8Array): Uint8Array | null {
try {
return x25519.getPublicKey(sk);
} catch {
return null;
}
}
```
## API Reference
All functions throw `PrimitiveNotImplemented("x25519")`:
- `getPublicKey(_privateKey: Uint8Array): Uint8Array`
- `getSharedSecret(_privateKey: Uint8Array, _peerPublicKey: Uint8Array): Uint8Array`
- `randomPrivateKey(): Uint8Array`
## Implementation Notes
Underscore-prefixed parameters suppress unused-parameter diagnostics without inline suppressions. When the native implementation is ready and RFC 7748 §6.1 test vectors pass, the `throw` is replaced with the real algorithm and the selector regenerated. The native backend for X25519 is particularly valuable in browser environments where `SubtleCrypto.deriveBits` with `{ name: "X25519" }` is available natively in modern browsers (Chrome 113+, Firefox 117+), enabling hardware-accelerated key agreement without a JavaScript implementation.
### Status
Placeholder native stub — throws `PrimitiveNotImplemented` until a WebCrypto `ECDH X25519` / Node `crypto.diffieHellman` bridge is added. Today the `x25519.select` selector always lands on `@noble/curves/ed25519` (which also provides the Curve25519 implementation); RFC 7748 test vectors in the KAT harness cover that path end-to-end.

View File

@@ -0,0 +1,71 @@
# x25519.noble
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`x25519.noble.ts` is the Noble-backed concrete implementation of X25519 in `@suaveplan/crypto-curve`. It wraps the `x25519` object from `@noble/curves/ed25519`, which shares the Curve25519 field arithmetic with Ed25519 but uses Montgomery form for Diffie-Hellman.
X25519 (RFC 7748 §5) computes a scalar multiplication on Curve25519 in Montgomery u-coordinate representation. The result is a 32-byte shared secret that both parties compute independently from their private key and the peer's public key. The output is a raw field element and must be processed through a KDF before use as a symmetric key.
## Features
- `getPublicKey(privateKey)` — derives the 32-byte Montgomery public key (u-coordinate of base-point multiple).
- `getSharedSecret(privateKey, peerPublicKey)` — ECDH shared secret; Noble handles RFC 7748 clamping internally.
- `randomPrivateKey()` — 32 random bytes; no rejection loop needed since every byte sequence is valid.
- Noble handles the RFC 7748 §5 private-key clamping (bits 0–2 of byte 0 cleared, bit 7 of byte 31 cleared, bit 6 set) internally during the scalar multiply.
## Usage: Basic Example
```ts
import * as x25519 from "@suaveplan/crypto-curve/x25519";
const sk = x25519.randomPrivateKey();
const pk = x25519.getPublicKey(sk);
console.log(pk.length); // 32
```
## Usage: Advanced Example
```ts
import * as x25519 from "@suaveplan/crypto-curve/x25519";
import { hkdf } from "@suaveplan/crypto-hash/kdf";
// Derive a 32-byte AES key from an X25519 shared secret.
function ecdheKey(localSk: Uint8Array, remotePk: Uint8Array): Uint8Array {
const raw = x25519.getSharedSecret(localSk, remotePk);
return hkdf("SHA-256", raw, {
salt: new Uint8Array(32),
info: new TextEncoder().encode("x25519-ecdhe"),
length: 32,
});
}
```
## API Reference
### `getPublicKey(privateKey: Uint8Array): Uint8Array`
| Parameter | Type | Description |
|-----------|------|-------------|
| `privateKey` | `Uint8Array` | Any 32 bytes. Noble applies RFC 7748 clamping. |
**Returns:** 32-byte Montgomery u-coordinate public key.
### `getSharedSecret(privateKey: Uint8Array, peerPublicKey: Uint8Array): Uint8Array`
| Parameter | Type | Description |
|-----------|------|-------------|
| `privateKey` | `Uint8Array` | 32-byte local private key. |
| `peerPublicKey` | `Uint8Array` | 32-byte remote public key. |
**Returns:** 32-byte raw shared secret (must be KDF-processed before use).
### `randomPrivateKey(): Uint8Array`
Returns 32 cryptographically random bytes from `@suaveplan/crypto-utils/random`.
## Implementation Notes
Noble's X25519 implementation auto-clamps private keys according to RFC 7748 §5, which means the actual scalar used is deterministic from the 32-byte input but differs from it. This clamping is intentional (it forces the scalar to be in a safe sub-group range) and callers should not attempt to apply clamping themselves. The shared secret is not safe to use as a key directly — apply HKDF-SHA-256 or equivalent. Swapping `getPublicKey` base with something other than the Curve25519 generator is not supported at this layer.

View File

@@ -0,0 +1,74 @@
# x25519
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
- [x25519.native](./impl/x25519.native/x25519.native.md) — <!-- begin:auto-docs --> _(no submodules)_- [x25519.noble](./impl/x25519.noble/x25519.noble.md) — <!-- begin:auto-docs --> _(no submodules)_ - [x25519.noble](./impl/x25519.noble/x25519.noble.md) — <!-- begin:auto-docs --> _(no submodules)_- [x25519.select](./x25519.select/x25519.select.md) — <!-- begin:auto-docs --> _(no submodules)_ - [x25519.select](./x25519.select/x25519.select.md) — <!-- begin:auto-docs --> _(no submodules)_- [x25519](./x25519.md) — <!-- begin:auto-docs --> _(no submodules)_- x25519.native — <!-- begin:auto-docs --> _(no submod…
- [x25519.native](./impl/x25519.native/x25519.native.md) — <!-- begin:auto-docs --> _(no submodules)_- [x25519.noble](./impl/x25519.noble/x25519.noble.md) — <!-- begin:auto-docs --> _(no submodules)_- [x25519.noble](./impl/x25519.noble/x25519.noble.md) — <!-- begin:auto-docs --> _(no submodules)_- [x25519.select](./x25519.select/x25519.select.md) — <!-- begin:auto-docs --> _(no submodules)_- [x25519.select](./x25519.select/x25519.select.md) — <!-- begin:auto-docs --> _(no submodules)_- [x25519](./x25519.md) — <!-- begin:auto-docs --> _(no submodules)_- [x25519.native](./impl/x25519.native/x25519.native.md) — <!-- begin:auto-docs --> _(no submodules)_- [x25519.noble](./impl/x25519.noble/x25519.noble.md) — <!-- begin:auto-docs --> _(no submodules)_- [x25519.noble](./impl/x25519.noble/x25519.noble.md) — <!-- begin:auto-docs --> _(no submodules)_- [x25519.select](./x25519.select/x25519.select.md) — <!-- begin:auto-docs --> _(no submodules)_- [x25519.select](./x25519.select/x25519.select.md) — <!-- begin:auto-docs --> _(no submodules)_- [x25519](./x25519.md) — <!-- begin:auto-docs --> _(no submodules)_- x25519.native — <!-- begin:a…
- [x25519.native](./impl/x25519.native/x25519.native.md) — <!-- begin:auto-docs --> _(no submodules)_- [x25519.noble](./impl/x25519.noble/x25519.noble.md) — <!-- begin:auto-docs --> _(no submodules)_- [x25519.noble](./impl/x25519.noble/x25519.noble.md) — <!-- begin:auto-docs --> _(no submodules)_- [x25519.select](./x25519.select/x25519.select.md) — <!-- begin:auto-docs --> _(no submodules)_- [x25519.select](./x25519.select/x25519.select.md) — <!-- begin:auto-docs --> _(no submodules)_- [x25519](./x25519.md) — <!-- begin:auto-docs --> _(no submodules)_- x25519.native — <!-- begin:auto-docs --> _(no submo…
- [x25519.native](./impl/x25519.native/x25519.native.md) — <!-- begin:auto-docs --> _(no submodules)_- [x25519.noble](./impl/x25519.noble/x25519.noble.md) — <!-- begin:auto-docs --> _(no submodules)_ - [x25519.noble](./impl/x25519.noble/x25519.noble.md) — <!-- begin:auto-docs --> _(no submodules)_- [x25519.select](./x25519.select/x25519.select.md) — <!-- begin:auto-docs --> _(no submodules)_ - [x25519.select](./x25519.select/x25519.select.md) — <!-- begin:auto-docs --> _(no submodules)_- [x25519](./x25519.md) — <!-- begin:auto-docs --> _(no submodules)_
## Purpose
`x25519.ts` is the stable public entry point for the X25519 elliptic-curve Diffie-Hellman primitive in `@suaveplan/crypto-curve`. It re-exports everything from `x25519.select.ts`, which forwards to either the Noble-backed or native implementation based on `crypto.config.ts`.
X25519 (RFC 7748) is the Diffie-Hellman function over Curve25519 in Montgomery form. It is the standard for ephemeral key agreement in TLS 1.3, Signal, Noise Protocol, and SSH. Its defining property is that any 32-byte value is a valid private key (no validity check required), and the output is a 32-byte shared secret that must be passed through a KDF before use as a symmetric key.
## Features
- `getPublicKey(privateKey)` — 32-byte Montgomery-form public key (u-coordinate).
- `getSharedSecret(privateKey, peerPublicKey)` — 32-byte raw ECDH output. Must be KDF-processed.
- `randomPrivateKey()` — 32 cryptographically random bytes (any 32-byte value is valid).
## Usage: Basic Example
```ts
import * as x25519 from "@suaveplan/crypto-curve/x25519";
const aliceSk = x25519.randomPrivateKey();
const alicePk = x25519.getPublicKey(aliceSk);
const bobSk = x25519.randomPrivateKey();
const bobPk = x25519.getPublicKey(bobSk);
const aliceShared = x25519.getSharedSecret(aliceSk, bobPk);
const bobShared = x25519.getSharedSecret(bobSk, alicePk);
// aliceShared and bobShared are identical
```
## Usage: Advanced Example
```ts
import * as x25519 from "@suaveplan/crypto-curve/x25519";
import { hkdf } from "@suaveplan/crypto-hash/kdf";
import { zeroize } from "@suaveplan/crypto-utils/zeroize";
// Ephemeral ECDH with HKDF key derivation (Noise XX pattern excerpt).
function deriveSessionKey(
localSk: Uint8Array,
remotePk: Uint8Array,
handshakeHash: Uint8Array
): Uint8Array {
const rawSecret = x25519.getSharedSecret(localSk, remotePk);
try {
return hkdf("SHA-256", rawSecret, {
salt: handshakeHash,
info: new TextEncoder().encode("noise-session"),
length: 32,
});
} finally {
zeroize(rawSecret);
}
}
```
## API Reference
All symbols re-exported from `x25519.select.ts`:
- `getPublicKey(privateKey: Uint8Array): Uint8Array`
- `getSharedSecret(privateKey: Uint8Array, peerPublicKey: Uint8Array): Uint8Array`
- `randomPrivateKey(): Uint8Array`
## Implementation Notes
This file is a zero-logic re-export barrel. Its sole purpose is the stable import path `@suaveplan/crypto-curve/x25519`. Logic belongs in the implementation files. The raw ECDH output of X25519 is not safe to use directly as a symmetric key — always run it through HKDF or another KDF before using for encryption or MAC operations.

View File

@@ -0,0 +1,54 @@
# x25519.select
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`x25519.select.ts` is the build-time primitive selector for X25519 in `@suaveplan/crypto-curve`. Generated by `bun scripts/gen-crypto-selects.ts`, it re-exports one backend — `x25519.noble.ts` or `x25519.native.ts` — based on the `x25519` entry in `crypto.config.ts`.
Callers import through `x25519.ts`, which delegates here. The selector isolates callers from backend changes: switching from Noble to a native implementation requires only regenerating this file. Currently the Noble backend is active because the native stub throws `PrimitiveNotImplemented`.
## Features
- Single re-export of `getPublicKey`, `getSharedSecret`, and `randomPrivateKey`.
- Generated — never hand-edited.
- Controlled by `crypto.config.ts`: set `x25519` to `'noble'` or `'native'`.
- Enables dead-code elimination of the inactive backend.
## Usage: Basic Example
```ts
import * as x25519 from "@suaveplan/crypto-curve/x25519";
const sk = x25519.randomPrivateKey();
const pk = x25519.getPublicKey(sk);
console.log(pk.length); // 32
```
## Usage: Advanced Example
```ts
import * as x25519 from "@suaveplan/crypto-curve/x25519";
// Simple ECDH exchange.
function exchange(localSk: Uint8Array, remotePk: Uint8Array): Uint8Array {
return x25519.getSharedSecret(localSk, remotePk);
}
```
## API Reference
Re-exports from the active implementation:
- `getPublicKey(privateKey: Uint8Array): Uint8Array` — 32-byte public key.
- `getSharedSecret(privateKey: Uint8Array, peerPublicKey: Uint8Array): Uint8Array` — 32-byte raw shared secret.
- `randomPrivateKey(): Uint8Array` — 32 random bytes.
## Implementation Notes
This file is a generated one-liner: `export * from "./impl/x25519.noble.js"`. The generator resolves the configured backend and writes the appropriate path. The file is idempotent and safe to regenerate at any time. Do not add logic here — it will be overwritten.
### Selection Logic
At build time `crypto.config.ts` picks either the Noble or the native engine for every primitive; the selector exported here reads the generated `crypto-selects` map and routes calls to the chosen impl. When the selected engine is not yet implemented, the `PrimitiveNotImplemented` sentinel from `@suaveplan/crypto-utils` is thrown with the primitive-id baked in so logs identify the exact missing slot.

View File

@@ -0,0 +1,7 @@
# @suaveplan/crypto-hash Changelog
## 0.0.0 — suaveplan
Initial suaveplan implementation. Ships SHA-2 digests, HMAC, HKDF, PBKDF2,
Argon2id, hex encoding, and Base64/Base64Url encoding. All functions are
pure TypeScript with zero WASM and zero browser globals.

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,125 @@
# base64
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Provides pure-TypeScript Base64 encoding and decoding in both standard (RFC
4648 §4) and URL-safe (RFC 4648 §5) variants. Cryptographic output such as
JWT payloads, bearer tokens, and encoded public keys is routinely transmitted
as Base64Url, while storage formats and MIME payloads use standard Base64.
This module implements both without depending on Node.js `Buffer` or `btoa`/
`atob` (which are absent in some edge runtimes), making it safe for all
`@suaveplan/universal` consumers.
## Features
- `toBase64(bytes)` — standard Base64 with `=` padding
- `fromBase64(b64)` — decode standard Base64; throws `HashError` on malformed input
- `toBase64Url(bytes)` — URL-safe Base64, no padding, `-` and `_` alphabet
- `fromBase64Url(b64url)` — decode URL-safe Base64; throws `HashError` on malformed input
- Passes RFC 4648 test vectors
- Pure TypeScript — no `Buffer`, no `btoa`/`atob`, no Node built-ins
## Usage: Basic Example
```ts
import { toBase64, fromBase64 } from "@suaveplan/crypto-hash/encoding";
const encoder = new TextEncoder();
const bytes = encoder.encode("hello world");
const b64 = toBase64(bytes);
console.log(b64); // "aGVsbG8gd29ybGQ="
const decoded = fromBase64(b64);
console.log(new TextDecoder().decode(decoded)); // "hello world"
```
## Usage: Advanced Example
```ts
import { toBase64Url, fromBase64Url } from "@suaveplan/crypto-hash/encoding";
import { sha256 } from "@suaveplan/crypto-hash/hash";
import { HashError } from "@suaveplan/crypto-hash/errors";
// Build a simple JWT-style header.payload structure
function buildToken(payload: object, sigKey: Uint8Array): string {
const encoder = new TextEncoder();
const header = toBase64Url(encoder.encode(JSON.stringify({ alg: "HS256" })));
const body = toBase64Url(encoder.encode(JSON.stringify(payload)));
return `${header}.${body}`;
}
// Parse and validate a Base64Url segment
function decodeSegment(segment: string): object {
try {
const bytes = fromBase64Url(segment);
return JSON.parse(new TextDecoder().decode(bytes)) as object;
} catch (err) {
if (err instanceof HashError) {
throw new TypeError(`Malformed Base64Url segment: ${err.code}`);
}
throw err;
}
}
const token = buildToken({ sub: "user-123", iat: 1700000000 }, new Uint8Array(32));
console.log(token);
```
## API Reference
### `toBase64(bytes: Uint8Array): string`
Encodes `bytes` as standard Base64 with `=` padding.
| Parameter | Type | Description |
|-----------|------|-------------|
| `bytes` | `Uint8Array` | Byte sequence to encode |
**Returns:** Base64 string of length `Math.ceil(bytes.length / 3) * 4`.
---
### `fromBase64(b64: string): Uint8Array`
Decodes a standard Base64 string. Padding (`=`) is optional.
| Parameter | Type | Description |
|-----------|------|-------------|
| `b64` | `string` | Standard Base64 string |
**Returns:** Decoded `Uint8Array`.
**Throws:** `HashError("HASH.ENCODING.INVALID_INPUT")` if any character is
outside the standard Base64 alphabet. Context: `{ encoding: "base64", input }`.
---
### `toBase64Url(bytes: Uint8Array): string`
Encodes `bytes` as URL-safe Base64 without padding. Uses `-` and `_` in place
of `+` and `/`.
---
### `fromBase64Url(b64url: string): Uint8Array`
Decodes a URL-safe Base64 string. `+` and `/` characters are rejected.
**Throws:** `HashError("HASH.ENCODING.INVALID_INPUT")` on invalid input.
Context: `{ encoding: "base64url", input }`.
## Implementation Notes
The encoder uses a 64-character alphabet lookup table and processes three bytes
at a time, producing four output characters per group. The decoder validates
every character code against a 128-entry lookup table before converting, so
malformed input is caught in a single pass rather than producing incorrect
output silently. The table approach avoids repeated `indexOf` calls and keeps
decoding O(n). Standard and URL-safe alphabets share the same encode/decode
pipeline via the `chars` and `table` parameters; `buildTable` is called once
at module load time and the resulting `Uint8Array` tables are module-level
constants. No `Buffer`, `btoa`, `atob`, or Node.js built-ins are used.

View File

@@ -0,0 +1,107 @@
# hex
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Provides `toHex` and `fromHex` for converting between `Uint8Array` byte
sequences and lowercase hexadecimal strings. These functions are the standard
representation layer for cryptographic output — digests, keys, tags, and
derived key material are routinely logged, compared, and stored as hex strings.
This module uses `@noble/hashes/utils` for the encoding direction and adds
strict input validation to `fromHex` so malformed strings produce a structured
`HashError` rather than silently corrupting data.
## Features
- `toHex(bytes)` — converts `Uint8Array` to lowercase hex string
- `fromHex(hex)` — parses lowercase or uppercase hex; throws on malformed input
- Throws `HashError("HASH.ENCODING.INVALID_INPUT")` on odd-length or non-hex input
- Accepts both lowercase and uppercase hex characters on decode
- Handles empty input correctly (empty array ↔ empty string)
## Usage: Basic Example
```ts
import { toHex, fromHex } from "@suaveplan/crypto-hash/encoding";
import { sha256 } from "@suaveplan/crypto-hash/hash";
const digest = sha256(new TextEncoder().encode("hello"));
const hex = toHex(digest);
console.log(hex); // 2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c...
const back = fromHex(hex);
console.log(back.length); // 32
```
## Usage: Advanced Example
```ts
import { toHex, fromHex } from "@suaveplan/crypto-hash/encoding";
import { hmac } from "@suaveplan/crypto-hash/mac";
import { HashError } from "@suaveplan/crypto-hash/errors";
function parseAndVerifyTag(
rawTagHex: string,
key: Uint8Array,
payload: Uint8Array
): boolean {
let receivedTag: Uint8Array;
try {
receivedTag = fromHex(rawTagHex);
} catch (err) {
if (err instanceof HashError) {
console.error("invalid tag format:", err.context);
return false;
}
throw err;
}
const expected = hmac("SHA-256", key, payload);
if (expected.length !== receivedTag.length) return false;
let diff = 0;
for (let i = 0; i < expected.length; i++) {
diff |= expected[i] ^ receivedTag[i];
}
return diff === 0;
}
```
## API Reference
### `toHex(bytes: Uint8Array): string`
Converts `bytes` to a lowercase hexadecimal string. Each byte becomes exactly
two hex characters; empty input returns `""`.
| Parameter | Type | Description |
|-----------|------|-------------|
| `bytes` | `Uint8Array` | Byte sequence to encode |
**Returns:** Lowercase hex string of length `bytes.length * 2`.
---
### `fromHex(hex: string): Uint8Array`
Parses a hexadecimal string (case-insensitive) into a `Uint8Array`.
| Parameter | Type | Description |
|-----------|------|-------------|
| `hex` | `string` | Hex-encoded string (may be upper- or lowercase) |
**Returns:** `Uint8Array` of length `hex.length / 2`.
**Throws:** `HashError("HASH.ENCODING.INVALID_INPUT")` if `hex.length` is odd
or if any character is outside `[0-9a-fA-F]`. The error `context` object
includes `{ encoding: "hex", input: <truncated input> }`.
## Implementation Notes
`toHex` delegates to `bytesToHex` from `@noble/hashes/utils`, which is a
well-tested, allocation-efficient implementation. `fromHex` validates the input
with a regular expression before calling `hexToBytes` — this ensures that
malformed strings throw a structured `HashError` rather than silently producing
garbage or throwing an untyped `Error`. The validation regex matches the full
string in one pass so there is no partial-result risk. The input is truncated to
32 characters in the error context to avoid logging full hex-encoded secrets.

View File

@@ -0,0 +1,75 @@
# Hash Error Codes
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Defines the stable error-code registry for the `@suaveplan/crypto-hash` package using `defineCodes` from `@suaveplan/error`. Every error thrown by the hash, MAC, and KDF modules carries one of these codes, giving callers a machine-readable signal they can match without inspecting human-readable message strings.
The code namespace `"HASH"` is unique within the project, ensuring that error codes do not collide with codes defined by other packages such as `@suaveplan/crypto-cipher` or `@suaveplan/crypto-curve`.
## Features
- `HASH_CODES.KDF_INVALID_PARAMS` — emitted when KDF parameters (iteration count, memory cost, output length) fall below required minima.
- `HASH_CODES.ALGORITHM_UNSUPPORTED` — emitted when a caller requests an algorithm string that is not recognised by the public dispatch layer.
- `HASH_CODES.ENCODING_INVALID_INPUT` — emitted by encoding utilities (hex, base64) when the input byte sequence or string is not valid for the requested encoding.
- `HashCode` union type — narrows the set of codes accepted by `HashError` constructors.
## Usage: Basic Example
```ts
import { HASH_CODES } from "@suaveplan/crypto-hash/errors";
// Check which code an error carries.
import { HashError } from "@suaveplan/crypto-hash/errors";
function handleHashError(err: HashError) {
if (err.code === HASH_CODES.KDF_INVALID_PARAMS) {
console.error("KDF parameters are out of range");
}
}
```
## Usage: Advanced Example
```ts
import { HASH_CODES, HashError } from "@suaveplan/crypto-hash/errors";
// Construct a typed error directly with the code registry.
function assertIterations(count: number): void {
if (count < 600_000) {
throw new HashError(
HASH_CODES.KDF_INVALID_PARAMS,
"error.hash.kdf.invalid-params",
{ context: { received: count, minimum: 600_000 } }
);
}
}
```
## API Reference
### `HASH_CODES`
```ts
const HASH_CODES: {
readonly KDF_INVALID_PARAMS: "HASH.KDF.INVALID_PARAMS";
readonly ALGORITHM_UNSUPPORTED: "HASH.ALGORITHM.UNSUPPORTED";
readonly ENCODING_INVALID_INPUT: "HASH.ENCODING.INVALID_INPUT";
};
```
Frozen registry object produced by `defineCodes("HASH", [...])`. The keys are screaming-snake-case identifiers; the values are the dot-separated canonical codes used in error serialisation and log queries.
### `HashCode`
```ts
type HashCode = (typeof HASH_CODES)[keyof typeof HASH_CODES];
```
Union of every string literal value in `HASH_CODES`. Used as the first type parameter to `SuaveplanError<HashCode>`.
## Implementation Notes
`defineCodes` from `@suaveplan/error` takes a namespace prefix and an array of sub-codes, joins them with a period, and returns a frozen map. The frozen map is both a runtime constant (safe to use in `switch` statements) and a type-level constant (the literal values flow through to `HashCode`). Adding a new code requires extending the `as const` array and re-running the TypeScript compiler; no numeric ID assignment or migration is needed.

View File

@@ -0,0 +1,79 @@
# HashError
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`HashError` is the package-local error class for `@suaveplan/crypto-hash`. It extends `SuaveplanError<HashCode>` from `@suaveplan/error`, inheriting the full structured-error contract: machine-readable code, i18n message key, cause-chain traversal, Zod-validated wire format, and OTel span-event adaptation.
The class exists to widen the protected `SuaveplanError` constructor to `public`, which lets primitive implementations and public façade functions throw without needing access to the base class internals. This is the canonical pattern used across every `@suaveplan/*` package that defines its own error codes.
## Features
- Typed on `HashCode` — compiler enforces that only codes from `HASH_CODES` are passed.
- Full cause-chain via `options.cause` — wraps lower-level errors (e.g. `PrimitiveNotImplemented`) without losing stack information.
- Structured context via `options.context` — attach key/value metadata (algorithm name, expected vs. actual lengths) for observability.
- Compatible with `SuaveplanError` cause-chain traversal utilities in `@suaveplan/error`.
- Serialisable to JSON wire format for cross-boundary error transport.
## Usage: Basic Example
```ts
import { HashError } from "@suaveplan/crypto-hash/errors";
import { HASH_CODES } from "@suaveplan/crypto-hash/errors";
throw new HashError(
HASH_CODES.KDF_INVALID_PARAMS,
"error.hash.kdf.invalid-params"
);
```
## Usage: Advanced Example
```ts
import { HashError } from "@suaveplan/crypto-hash/errors";
import { HASH_CODES } from "@suaveplan/crypto-hash/errors";
function assertOutputLength(length: number): void {
if (length < 1 || length > 255 * 32) {
throw new HashError(
HASH_CODES.KDF_INVALID_PARAMS,
"error.hash.kdf.invalid-params",
{
cause: new RangeError(`length ${length} out of bounds`),
context: {
field: "length",
received: length,
min: 1,
max: 255 * 32,
},
}
);
}
}
```
## API Reference
### `class HashError extends SuaveplanError<HashCode>`
#### `constructor(code: HashCode, messageKey: string, options?: SuaveplanErrorOptions)`
| Parameter | Type | Description |
|-----------|------|-------------|
| `code` | `HashCode` | One of the codes from `HASH_CODES`. |
| `messageKey` | `string` | Dotted i18n key used by the message resolver hook. |
| `options` | `SuaveplanErrorOptions` (optional) | `cause`, `context`, and other base-class options. |
Inherits all public members of `SuaveplanError<HashCode>`:
- `code: HashCode`
- `messageKey: string`
- `context?: Record<string, unknown>`
- `toJSON(): unknown` — Zod-validated wire representation.
- `toSpanEvent(): OTelSpanEvent` — OpenTelemetry span event.
## Implementation Notes
The constructor body is intentionally empty; it exists only to satisfy the TypeScript visibility requirement that converts `protected` (the base class default) to `public`. Biome's `noUselessConstructor` rule is suppressed with an inline ignore comment for this reason — the constructor is semantically required even though it contains no logic. The pattern is consistent with `CryptoCipherError` and the error classes in every other `@suaveplan/crypto-*` package.

View File

@@ -0,0 +1,111 @@
# sha2
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Provides one-shot SHA-2 digest functions (`sha256`, `sha384`, `sha512`) and a
`hash` dispatcher that selects the algorithm by name. All functions are thin,
allocation-friendly wrappers around the audited `@noble/hashes` implementation —
zero WASM, zero side effects, fully tree-shakeable.
The module exists as an independent substrate so that higher-level packages
(`@suaveplan/crypto-hash`, `@suaveplan/crypto`) can import SHA-2 digests without
pulling in unrelated cryptographic primitives.
## Features
- `sha256(data)` — 32-byte SHA-256 digest
- `sha384(data)` — 48-byte SHA-384 digest
- `sha512(data)` — 64-byte SHA-512 digest
- `hash(algo, data)` — single-call dispatch over `HashAlgorithm`
- `HashAlgorithm` union type (`"SHA-256" | "SHA-384" | "SHA-512"`)
- Deterministic: same input always produces the same output
- Zero runtime state; safe for concurrent use
## Usage: Basic Example
```ts
import { sha256 } from "@suaveplan/crypto-hash/hash";
const encoder = new TextEncoder();
const digest = sha256(encoder.encode("hello world"));
console.log(digest.length); // 32
```
## Usage: Advanced Example
```ts
import { hash, type HashAlgorithm } from "@suaveplan/crypto-hash/hash";
import { toHex } from "@suaveplan/crypto-hash/encoding";
function fingerprintFile(
algo: HashAlgorithm,
content: Uint8Array
): string {
return `${algo.toLowerCase()}:${toHex(hash(algo, content))}`;
}
const encoder = new TextEncoder();
console.log(fingerprintFile("SHA-256", encoder.encode("document payload")));
// sha-256:a591a6d40bf420404a011733cfb7b190d62c65bf0bcda32b57b277d9ad9f146e
```
## API Reference
### `sha256(data: Uint8Array): Uint8Array`
Computes the SHA-256 digest of `data`. Returns a fresh 32-byte `Uint8Array`.
| Parameter | Type | Description |
|-----------|------|-------------|
| `data` | `Uint8Array` | Arbitrary byte sequence to hash |
**Returns:** 32-byte digest.
---
### `sha384(data: Uint8Array): Uint8Array`
Computes the SHA-384 digest of `data`. Returns a fresh 48-byte `Uint8Array`.
---
### `sha512(data: Uint8Array): Uint8Array`
Computes the SHA-512 digest of `data`. Returns a fresh 64-byte `Uint8Array`.
---
### `hash(algo: HashAlgorithm, data: Uint8Array): Uint8Array`
Dispatch wrapper. Selects the underlying function by `algo` and returns the
digest. Useful when the algorithm is determined at runtime.
| Parameter | Type | Description |
|-----------|------|-------------|
| `algo` | `HashAlgorithm` | `"SHA-256"`, `"SHA-384"`, or `"SHA-512"` |
| `data` | `Uint8Array` | Arbitrary byte sequence to hash |
**Returns:** Variable-length digest (32 / 48 / 64 bytes).
---
### `HashAlgorithm`
```ts
type HashAlgorithm = "SHA-256" | "SHA-384" | "SHA-512";
```
String union that gates which algorithms the rest of the hash package accepts.
Aligns with Web Crypto API naming conventions for cross-platform consistency.
## Implementation Notes
All three functions delegate directly to `@noble/hashes/sha2`, which is an
extensively audited, pure-JavaScript SHA-2 implementation. The wrappers add no
observable overhead beyond a single function call. The `hash` dispatcher uses
two explicit `if` checks and a fallthrough to `sha512`, so the TypeScript
exhaustiveness checker catches future additions to `HashAlgorithm` at the call
site. No WASM is loaded; no global state is touched.

View File

@@ -0,0 +1,151 @@
# argon2id
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Provides Argon2id password hashing as specified in RFC 9106. Argon2id is the
current winner of the Password Hashing Competition and is recommended by OWASP
for new applications. It is resistant to both GPU-based brute force (via high
memory cost) and time-memory trade-off attacks (via the hybrid id variant).
This module enforces non-pathological lower bounds on all four tuning parameters
and produces a structured `HashError` with per-field context when they are
violated, making it safe for frameworks to call without a separate validation
layer.
## Features
- `argon2id(password, options)` — Argon2id key derivation
- Exported constants: `ARGON2ID_MIN_MEMORY_COST_KIB`, `ARGON2ID_MIN_TIME_COST`, `ARGON2ID_MIN_PARALLELISM`
- Validates each parameter individually; error context includes `{ field, value, minimum }`
- Throws `HashError("HASH.KDF.INVALID_PARAMS")` for any out-of-range parameter
- Deterministic: same inputs always produce the same hash output
## Usage: Basic Example
```ts
import { argon2id, ARGON2ID_MIN_MEMORY_COST_KIB } from "@suaveplan/crypto-hash/kdf";
const encoder = new TextEncoder();
const password = encoder.encode("hunter2");
const salt = new Uint8Array(16); // use crypto.getRandomValues in production
const hash = argon2id(password, {
salt,
timeCost: 3,
memoryCost: ARGON2ID_MIN_MEMORY_COST_KIB * 1024, // 8 MiB in KiB
parallelism: 1,
length: 32,
});
console.log(hash.length); // 32
```
## Usage: Advanced Example
```ts
import {
argon2id,
ARGON2ID_MIN_MEMORY_COST_KIB,
ARGON2ID_MIN_TIME_COST,
ARGON2ID_MIN_PARALLELISM,
} from "@suaveplan/crypto-hash/kdf";
import { toHex } from "@suaveplan/crypto-hash/encoding";
import { HashError } from "@suaveplan/crypto-hash/errors";
interface PasswordHashConfig {
timeCost: number;
memoryCostKib: number;
parallelism: number;
outputBytes: number;
}
function hashPassword(password: string, salt: Uint8Array, cfg: PasswordHashConfig): string {
try {
const raw = argon2id(new TextEncoder().encode(password), {
salt,
timeCost: cfg.timeCost,
memoryCost: cfg.memoryCostKib,
parallelism: cfg.parallelism,
length: cfg.outputBytes,
});
return toHex(raw);
} catch (err) {
if (err instanceof HashError && err.context?.field) {
throw new RangeError(
`Argon2id parameter out of range: ${err.context.field} = ${err.context.value} ` +
`(minimum: ${err.context.minimum})`
);
}
throw err;
}
}
const salt = new Uint8Array(16);
const hex = hashPassword("secure-password", salt, {
timeCost: ARGON2ID_MIN_TIME_COST,
memoryCostKib: ARGON2ID_MIN_MEMORY_COST_KIB,
parallelism: ARGON2ID_MIN_PARALLELISM,
outputBytes: 32,
});
console.log(hex.length); // 64 hex chars = 32 bytes
```
## API Reference
### `argon2id(password: Uint8Array, options: Argon2idOptions): Uint8Array`
Derives `options.length` bytes from `password` using Argon2id.
| Parameter | Type | Description |
|-----------|------|-------------|
| `password` | `Uint8Array` | User-supplied password bytes |
| `options.salt` | `Uint8Array` | Per-credential random salt (≥ 16 bytes recommended) |
| `options.timeCost` | `number` | Integer ≥ `ARGON2ID_MIN_TIME_COST` (number of iterations) |
| `options.memoryCost` | `number` | Integer ≥ `ARGON2ID_MIN_MEMORY_COST_KIB` (in KiB) |
| `options.parallelism` | `number` | Integer ≥ `ARGON2ID_MIN_PARALLELISM` (lane count) |
| `options.length` | `number` | Positive integer byte count for derived output |
**Returns:** `Uint8Array` of `options.length` bytes.
**Throws:** `HashError("HASH.KDF.INVALID_PARAMS")` for any validation failure.
Error `context` contains `{ field: string, value: number, minimum: number }`.
---
### `ARGON2ID_MIN_MEMORY_COST_KIB = 8`
Minimum memory cost in KiB. Based on RFC 9106 §4 minimum recommendation.
### `ARGON2ID_MIN_TIME_COST = 1`
Minimum number of Argon2id iterations. Lower values are pathological.
### `ARGON2ID_MIN_PARALLELISM = 1`
Minimum lane count. Lower values are rejected as invalid.
---
### `Argon2idOptions`
```ts
interface Argon2idOptions {
readonly salt: Uint8Array;
readonly timeCost: number;
readonly memoryCost: number;
readonly parallelism: number;
readonly length: number;
}
```
## Implementation Notes
Parameters are validated in order: `timeCost`, `memoryCost`, `parallelism`,
`length`. The first failing field short-circuits and throws with that field's
context — no partial execution occurs. The implementation delegates to
`@noble/hashes/argon2` which is a pure-JavaScript Argon2id implementation
reviewed by the cryptographic community. Argon2id does not use the `HashAlgorithm`
dispatch pattern used by `hmac`, `hkdf`, and `pbkdf2` — Argon2id's internal
structure is fixed and parameterised by `t`, `m`, and `p` rather than a hash
constructor. For this reason, `argon2id.ts` does not import from `mac/hmac.ts`.

View File

@@ -0,0 +1,106 @@
# hkdf
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Provides HKDF (HMAC-based Key Derivation Function, RFC 5869) over SHA-256,
SHA-384, and SHA-512. HKDF is the standard mechanism for stretching or
diversifying key material — for instance, turning a Diffie-Hellman shared
secret into one or more symmetric encryption keys. This module wraps the
audited `@noble/hashes` HKDF implementation and constrains the algorithm
surface to the `HashAlgorithm` union shared across `@suaveplan/crypto-hash`.
## Features
- `hkdf(algo, ikm, options)` — HKDF-Extract-and-Expand in one call
- Supports SHA-256, SHA-384, SHA-512 via `HashAlgorithm`
- Configurable output length (in bytes)
- Accepts arbitrary `salt` and `info` context bytes
- Deterministic: same inputs always produce the same key material
## Usage: Basic Example
```ts
import { hkdf } from "@suaveplan/crypto-hash/kdf";
const ikm = new Uint8Array(32).fill(0x01); // input key material
const salt = new Uint8Array(16); // random salt (often public)
const info = new TextEncoder().encode("suaveplan-session-key-v1");
const sessionKey = hkdf("SHA-256", ikm, { salt, info, length: 32 });
console.log(sessionKey.length); // 32
```
## Usage: Advanced Example
```ts
import { hkdf } from "@suaveplan/crypto-hash/kdf";
import { toHex } from "@suaveplan/crypto-hash/encoding";
// Derive separate encryption and MAC keys from a single DH shared secret
function deriveKeyPair(sharedSecret: Uint8Array, context: string): {
encKey: Uint8Array;
macKey: Uint8Array;
} {
const encoder = new TextEncoder();
const salt = new Uint8Array(32); // zero salt; real usage uses random
const encKey = hkdf("SHA-256", sharedSecret, {
salt,
info: encoder.encode(`${context}:enc`),
length: 32,
});
const macKey = hkdf("SHA-256", sharedSecret, {
salt,
info: encoder.encode(`${context}:mac`),
length: 32,
});
return { encKey, macKey };
}
const { encKey, macKey } = deriveKeyPair(new Uint8Array(32), "chat-session");
console.log(toHex(encKey));
console.log(toHex(macKey));
```
## API Reference
### `hkdf(algo: HashAlgorithm, ikm: Uint8Array, options: HkdfOptions): Uint8Array`
Derives `options.length` bytes of key material from the input keying material
`ikm` using the two-step HKDF-Extract-and-Expand procedure.
| Parameter | Type | Description |
|-----------|------|-------------|
| `algo` | `HashAlgorithm` | Underlying hash: `"SHA-256"`, `"SHA-384"`, or `"SHA-512"` |
| `ikm` | `Uint8Array` | Input keying material (e.g., DH shared secret) |
| `options.salt` | `Uint8Array` | Optional salt bytes (use a random or domain-specific value) |
| `options.info` | `Uint8Array` | Context and application-specific information |
| `options.length` | `number` | Number of output bytes to derive |
**Returns:** `Uint8Array` of `options.length` bytes.
---
### `HkdfOptions`
```ts
interface HkdfOptions {
readonly salt: Uint8Array;
readonly info: Uint8Array;
readonly length: number;
}
```
## Implementation Notes
HKDF is defined in RFC 5869. The Extract step computes `PRK = HMAC(salt, ikm)`;
the Expand step iterates `HMAC(PRK, info || counter)` until `length` bytes are
produced. Both steps are performed by `@noble/hashes/hkdf` — no hand-rolled
HMAC is involved here. The internal `hasherFor` helper is a private module
function mapping `HashAlgorithm` strings to hash constructors; it is not
exported. The maximum derivable length is `255 × hashLen`, which for SHA-256 is
8160 bytes — well within any practical keying requirement.

View File

@@ -0,0 +1,128 @@
# pbkdf2
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Provides PBKDF2 (Password-Based Key Derivation Function 2, RFC 8018) over
SHA-2 variants. PBKDF2 derives a fixed-length key from a password by applying
an HMAC pseudo-random function iteratively — the iteration count is the primary
cost-tuning knob. This module enforces a hard floor of 600 000 iterations
(the OWASP 2023 minimum for PBKDF2-SHA-256) to prevent accidental use of
dangerously weak parameters. Callers that legitimately need lower counts for
legacy-system interoperability can call `@noble/hashes` directly.
## Features
- `pbkdf2(algo, password, options)` — PBKDF2 over SHA-256/384/512
- `PBKDF2_MIN_ITERATIONS = 600_000` — OWASP 2023 minimum, exported as a constant
- Throws `HashError("HASH.KDF.INVALID_PARAMS")` if `iterations < 600_000` or non-integer
- Throws `HashError("HASH.KDF.INVALID_PARAMS")` if `length ≤ 0` or non-integer
- Error context includes `{ field, value, minimum }` for programmatic handling
- Deterministic: same inputs always produce the same derived key
## Usage: Basic Example
```ts
import { pbkdf2, PBKDF2_MIN_ITERATIONS } from "@suaveplan/crypto-hash/kdf";
const encoder = new TextEncoder();
const password = encoder.encode("correct-horse-battery-staple");
const salt = new Uint8Array(16); // use crypto.getRandomValues in real code
const key = pbkdf2("SHA-256", password, {
salt,
iterations: PBKDF2_MIN_ITERATIONS,
length: 32,
});
console.log(key.length); // 32
```
## Usage: Advanced Example
```ts
import { pbkdf2, PBKDF2_MIN_ITERATIONS } from "@suaveplan/crypto-hash/kdf";
import { toHex } from "@suaveplan/crypto-hash/encoding";
import { HashError } from "@suaveplan/crypto-hash/errors";
function deriveStorageKey(
password: string,
saltHex: string,
lengthBytes = 32
): string {
const encoder = new TextEncoder();
const salt = new Uint8Array(
saltHex.match(/.{2}/g)!.map((b) => parseInt(b, 16))
);
try {
const key = pbkdf2("SHA-256", encoder.encode(password), {
salt,
iterations: PBKDF2_MIN_ITERATIONS,
length: lengthBytes,
});
return toHex(key);
} catch (err) {
if (err instanceof HashError) {
throw new Error(
`Key derivation failed: ${err.code} — ${JSON.stringify(err.context)}`
);
}
throw err;
}
}
```
## API Reference
### `pbkdf2(algo: HashAlgorithm, password: Uint8Array, options: Pbkdf2Options): Uint8Array`
Derives `options.length` bytes from `password` using PBKDF2.
| Parameter | Type | Description |
|-----------|------|-------------|
| `algo` | `HashAlgorithm` | Underlying hash: `"SHA-256"`, `"SHA-384"`, or `"SHA-512"` |
| `password` | `Uint8Array` | User-supplied password bytes |
| `options.salt` | `Uint8Array` | Per-credential random salt (≥ 16 bytes recommended) |
| `options.iterations` | `number` | Integer ≥ `PBKDF2_MIN_ITERATIONS` |
| `options.length` | `number` | Positive integer byte count for derived key |
**Returns:** `Uint8Array` of `options.length` bytes.
**Throws:** `HashError("HASH.KDF.INVALID_PARAMS")` when `iterations` or `length`
fail validation. The error `context` object includes `{ field, value, minimum? }`.
---
### `PBKDF2_MIN_ITERATIONS`
```ts
const PBKDF2_MIN_ITERATIONS = 600_000;
```
OWASP 2023 recommended minimum for PBKDF2-SHA-256. Exported so callers can
reference it when building configuration objects.
---
### `Pbkdf2Options`
```ts
interface Pbkdf2Options {
readonly salt: Uint8Array;
readonly iterations: number;
readonly length: number;
}
```
## Implementation Notes
PBKDF2 is defined in RFC 8018 (superseding RFC 2898). The function delegates
to `@noble/hashes/pbkdf2` and maps `HashAlgorithm` strings to hash constructors
via a private `hasherFor` function that is not exported. The 600 000-iteration
floor corresponds to OWASP's 2023 recommendation for PBKDF2-HMAC-SHA-256; the
NIST SP 800-132 guideline is 10 000 but OWASP's figure accounts for modern GPU
attack capabilities. Applications using SHA-384 or SHA-512 may consider lower
counts because those hashes are slower on commodity hardware, but this module
applies the same floor for all variants to avoid subtle configuration mistakes.

View File

@@ -0,0 +1,69 @@
# argon2id-password
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`argon2id-password.ts` provides a PHC-format password hashing scheme on top of the Argon2id primitive. The output format is `$argon2id$v=19$m=<memoryCost>,t=<timeCost>,p=<parallelism>$<saltB64>$<hashB64>` — the official Argon2id PHC string used by `argon2`-cli, libsodium, and most server-side authentication libraries. suaveplan emits the same format so vault tooling, server keystores, and migration scripts can interoperate without an adapter layer.
Argon2id is the recommended OWASP password hashing scheme. It is memory-hard (resistant to GPU/ASIC brute force), time-hard (configurable iteration count), and side-channel resistant (the "id" variant blends data-independent and data-dependent passes). This module wraps the suaveplan Argon2id primitive with the PHC string format, default OWASP-2023 parameters, and constant-time verification.
## Features
- PHC-format output `$argon2id$v=19$m=N,t=N,p=N$saltB64$hashB64` — interoperable with all standard Argon2id implementations.
- OWASP 2023 default parameters: `m=65536` (64 MiB), `t=3` iterations, `p=1` parallelism.
- Constant-time verification via `@suaveplan/crypto-utils::constantTimeEqual` over the 32-byte hash.
- 16-byte fresh random salt per hash call; deterministic when an explicit salt is supplied (test-only).
- Strict format validation: malformed strings throw `HashError(BLAKE2B_PASSWORD_FORMAT_INVALID)` (note: the same generic password-format error code is shared between Argon2id and BLAKE2b passwords; the message key disambiguates).
## Usage: Basic Example
```ts
import {
hashPasswordArgon2id,
verifyPasswordArgon2id,
} from "@suaveplan/crypto-hash/mac/argon2id-password/argon2id-password";
const stored = hashPasswordArgon2id("correct horse battery staple");
// → "$argon2id$v=19$m=65536,t=3,p=1$<salt>$<hash>"
const ok = verifyPasswordArgon2id(stored, "correct horse battery staple");
// → true
```
## Usage: Advanced Example
```ts
import { hashPasswordArgon2id, verifyPasswordArgon2id } from "@suaveplan/crypto-hash/mac/argon2id-password/argon2id-password";
// Adaptive cost: probe how long a verify takes on this hardware and
// pick the largest memory cost that still fits in a target latency.
function calibrate(password: string, targetMs: number): number {
let m = 16_384; // 16 MiB starting point
while (m <= 262_144) {
const stored = hashPasswordArgon2id(password, {
memoryCost: m,
timeCost: 3,
parallelism: 1,
});
const start = performance.now();
verifyPasswordArgon2id(stored, password);
if (performance.now() - start > targetMs) return m / 2;
m *= 2;
}
return 262_144;
}
```
## API Reference
- `hashPasswordArgon2id(password: string, options?: HashPasswordArgon2idOptions): string`.
- `verifyPasswordArgon2id(storedHash: string, password: string): boolean`.
- `HashPasswordArgon2idOptions { memoryCost?, timeCost?, parallelism?, salt? }` — all fields optional; defaults are OWASP 2023 recommendations.
Errors: `HASH.BLAKE2B_PASSWORD_FORMAT_INVALID` for malformed stored hashes (missing prefix, wrong segment count, bad params, bad base64).
## Implementation Notes
The PHC format uses base64-without-padding (per RFC 9106 Appendix B); the parser pads back to a multiple of 4 before decoding. Memory cost is in KiB, time cost is the number of passes, parallelism is the number of lanes — these are the same units the underlying Argon2id primitive consumes, so there is no unit conversion. Constant-time comparison happens on the raw 32-byte hash buffer; the salt and parameter components of the PHC string are not secret and do not need timing-safe handling. suaveplan re-uses the existing `BLAKE2B_PASSWORD_FORMAT_INVALID` error code rather than minting a new `ARGON2ID_*` code; both schemes are MCF-style password hashes and a unified format-error code keeps consumer dispatch simple.

View File

@@ -0,0 +1,72 @@
# blake2b-password
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`blake2b-password.ts` provides a password-hashing scheme built on the BLAKE2b primitive. It is the right tool when you need a fast, deterministic password hasher in a memory-constrained or platform-restricted environment where Argon2id is not available — for example, embedded contexts, WASM-cold-start-sensitive paths, or when interoperating with legacy systems that already store BLAKE2b-iterated password hashes.
The format is `$blake2b$v=1$cf=<costFactor>$<saltHex>$<hashHex>` — an MCF-style stored hash that captures the cost factor, salt, and hash bytes in a single deterministically-parseable string. The cost factor controls iteration count exponentially (`iterations = 2^costFactor`) so doubling it doubles the work; the default of `14` corresponds to 16,384 iterations of BLAKE2b-512.
This is **not a substitute for Argon2id**. Argon2id is memory-hard; BLAKE2b iteration is purely time-hard, which is weaker against attackers with massive parallel hardware (GPUs, FPGAs). Use Argon2id whenever you can; reach for this only when interop or platform constraints force it.
## Features
- Compact MCF-style stored format: `$blake2b$v=1$cf=N$salt$hash` — captures the algorithm tag, version, cost factor, salt, and hash in a single round-trippable string.
- `hashPasswordBlake2b(password, options?)` — generates a fresh 16-byte salt by default; deterministic when an explicit salt is supplied.
- `verifyPasswordBlake2b(stored, password)` — constant-time comparison via `@suaveplan/crypto-utils::constantTimeEqual`.
- Cost-factor range `1..31` (iteration counts 2..2^31). Outside that range, verification throws `HashError(BLAKE2B_PASSWORD_FORMAT_INVALID)`.
- Returns `false` for non-matching passwords without leaking timing information; throws structured errors only for malformed stored hashes.
## Usage: Basic Example
```ts
import {
hashPasswordBlake2b,
verifyPasswordBlake2b,
} from "@suaveplan/crypto-hash/mac/blake2b-password";
const stored = hashPasswordBlake2b("correct horse battery staple");
// → "$blake2b$v=1$cf=14$<salt-hex>$<hash-hex>"
const ok = verifyPasswordBlake2b(stored, "correct horse battery staple");
// → true
```
## Usage: Advanced Example
```ts
import { hashPasswordBlake2b, verifyPasswordBlake2b } from "@suaveplan/crypto-hash/mac/blake2b-password";
// Adaptive cost factor: probe how long a verify takes and tune cost.
async function calibrate(targetMs = 100): Promise<number> {
for (let cf = 12; cf <= 20; cf++) {
const stored = hashPasswordBlake2b("probe", { costFactor: cf });
const start = performance.now();
verifyPasswordBlake2b(stored, "probe");
if (performance.now() - start > targetMs) return cf;
}
return 20;
}
// Migrating a password that was stored at an older cost factor: detect
// the cost in the stored string and re-hash on successful login.
function needsRehash(storedHash: string, target: number): boolean {
const match = storedHash.match(/\$cf=(\d+)\$/);
return match ? Number(match[1]) < target : true;
}
```
## API Reference
- `hashPasswordBlake2b(password: string, options?: HashPasswordBlake2bOptions): string` — produces an MCF-style stored hash.
- `verifyPasswordBlake2b(storedHash: string, password: string): boolean` — constant-time verify.
- `HashPasswordBlake2bOptions { costFactor?: number; salt?: Uint8Array }` — `costFactor` defaults to `14`; `salt` defaults to a fresh 16-byte random buffer.
- `DEFAULT_COST_FACTOR = 14`, `SALT_LENGTH = 16`, `HASH_LENGTH = 64` constants.
Errors: `HASH.BLAKE2B_PASSWORD_FORMAT_INVALID` for malformed stored hashes (missing prefix, wrong segment count, missing `cf=` tag, out-of-range cost factor, malformed hex, wrong hash length).
## Implementation Notes
The iteration loop is BLAKE2b chained on its own output: `acc_0 = BLAKE2b(password ‖ salt)`, `acc_n = BLAKE2b(acc_{n-1})`. This is intentionally simple — it's not a memory-hard scheme like Argon2id, just an iterated hash. The cost factor is exponential (`iterations = 2^costFactor`) so the default of `14` gives 16,384 iterations, which is approximately `~5–10ms` on a modern desktop and `~50ms` on a slow mobile. Constant-time comparison happens on the raw 64-byte hash buffer via `constantTimeEqual`, which means the verify side leaks no information about which byte first differs. The cost factor is bounded at 31 to prevent integer-overflow surprises (`1 << 32` overflows to negative in JS bitwise math). Salts are caller-supplied via the `salt` option for tests; the default is `randomBytes(16)`.

View File

@@ -0,0 +1,97 @@
# hmac
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Provides HMAC (Hash-based Message Authentication Code) over SHA-2 variants.
HMAC proves that a message was produced by a party in possession of a shared
key, giving both data integrity and authenticity guarantees without asymmetric
cryptography. This module wraps the audited `@noble/hashes` HMAC primitive
and restricts the algorithm surface to the same `HashAlgorithm` union used
throughout `@suaveplan/crypto-hash`.
## Features
- `hmac(algo, key, data)` — one-shot HMAC-SHA-{256,384,512}
- Throws `HashError("HASH.ALGORITHM.UNSUPPORTED")` for unrecognised algorithm strings
- Passes through RFC 4231 HMAC-SHA-256 test vectors
- Deterministic: same `(algo, key, data)` triple always produces the same tag
- Zero runtime state; safe for concurrent use
## Usage: Basic Example
```ts
import { hmac } from "@suaveplan/crypto-hash/mac";
import { toHex } from "@suaveplan/crypto-hash/encoding";
const encoder = new TextEncoder();
const key = encoder.encode("super-secret-key");
const data = encoder.encode("payment:amount=100");
const tag = hmac("SHA-256", key, data);
console.log(toHex(tag)); // 32-byte hex string
```
## Usage: Advanced Example
```ts
import { hmac } from "@suaveplan/crypto-hash/mac";
import { toHex, fromHex } from "@suaveplan/crypto-hash/encoding";
// Verify a webhook signature in constant time
function verifyWebhookTag(
secret: Uint8Array,
payload: Uint8Array,
receivedTagHex: string
): boolean {
const expected = hmac("SHA-256", secret, payload);
const received = fromHex(receivedTagHex);
if (expected.length !== received.length) return false;
let diff = 0;
for (let i = 0; i < expected.length; i++) {
diff |= expected[i] ^ received[i];
}
return diff === 0;
}
const encoder = new TextEncoder();
const secret = encoder.encode("webhook-secret");
const body = encoder.encode('{"event":"charge.succeeded"}');
const tag = toHex(hmac("SHA-256", secret, body));
console.log(verifyWebhookTag(secret, body, tag)); // true
```
## API Reference
### `hmac(algo: HashAlgorithm, key: Uint8Array, data: Uint8Array): Uint8Array`
Computes the HMAC of `data` under `key` using the specified SHA-2 algorithm.
| Parameter | Type | Description |
|-----------|------|-------------|
| `algo` | `HashAlgorithm` | `"SHA-256"`, `"SHA-384"`, or `"SHA-512"` |
| `key` | `Uint8Array` | Shared secret key (any length ≥ 1 byte) |
| `data` | `Uint8Array` | Message to authenticate |
**Returns:** Tag bytes — 32 / 48 / 64 bytes for SHA-256 / SHA-384 / SHA-512
respectively.
**Throws:** `HashError("HASH.ALGORITHM.UNSUPPORTED")` if `algo` is not one of
the three supported variants.
## Implementation Notes
The internal `hasherFor` helper maps `HashAlgorithm` strings to the corresponding
`@noble/hashes` hash constructor. It is a private module function — not exported
from this file or from any barrel — so it cannot be called by consumers.
This design keeps the public API surface minimal while allowing `hkdf.ts` and
`pbkdf2.ts` to import `hasherFor` from this module directly for their own
internal use. `argon2id.ts` does not use `hasherFor` because Argon2id is a
password-hashing algorithm that accepts its own internal parameterisation rather
than a bare hash constructor.
HMAC output length equals the underlying hash output length. The `@noble/hashes`
implementation is constant-time within the hash primitive itself; callers are
responsible for constant-time comparison of the resulting tags.

View File

@@ -0,0 +1,98 @@
# argon2id.select
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`argon2id.select.ts` is the build-time primitive selector for the Argon2id implementation in `@suaveplan/crypto-hash`. It re-exports one concrete backend — either `argon2id.noble.ts` (backed by `@noble/hashes`) or `argon2id.native.ts` — based on the `argon2id` entry in `crypto.config.ts`.
Argon2id is the Password Hashing Competition winner and the recommendation of RFC 9106 for password hashing and key derivation. Its hybrid data-dependent and data-independent memory access pattern resists both GPU-parallel and side-channel attacks, making it the preferred choice when deriving keys from passwords or other low-entropy secrets. The selector exposes `argon2id` and `Argon2idOptions` through a stable interface.
## Features
- Re-exports `argon2id(password, options)` and `Argon2idOptions`.
- Generated by `bun scripts/gen-crypto-selects.ts` — never hand-edited.
- Controlled by `crypto.config.ts`: set `argon2id` to `'noble'` or `'native'`.
- `Argon2idOptions` carries `salt`, `timeCost`, `memoryCost`, `parallelism`, and `length`.
- RFC 9106 minimum parameter validation is enforced at the public façade, not here.
## Usage: Basic Example
```ts
import { argon2id } from "@suaveplan/crypto-hash/primitives/argon2id/argon2id";
const password = new TextEncoder().encode("correct-horse-battery-staple");
const salt = crypto.getRandomValues(new Uint8Array(16));
const hash = argon2id(password, {
salt,
timeCost: 3,
memoryCost: 65536,
parallelism: 4,
length: 32,
});
console.log(hash.length); // 32
```
## Usage: Advanced Example
```ts
import { argon2id, type Argon2idOptions } from "@suaveplan/crypto-hash/primitives/argon2id/argon2id";
// Interactive login profile (RFC 9106 minimum-t=1 adjusted for latency).
const INTERACTIVE_PARAMS: Argon2idOptions = {
salt: new Uint8Array(16), // placeholder; real callers supply random salt
timeCost: 2,
memoryCost: 19456, // ~19 MiB
parallelism: 1,
length: 32,
};
// Server-side profile (higher security, longer wait acceptable).
const SENSITIVE_PARAMS: Argon2idOptions = {
salt: new Uint8Array(16),
timeCost: 3,
memoryCost: 65536, // ~64 MiB
parallelism: 4,
length: 64,
};
function deriveKey(password: Uint8Array, opts: Argon2idOptions): Uint8Array {
return argon2id(password, opts);
}
```
## API Reference
Re-exports the following from the active implementation:
### `argon2id(password: Uint8Array, options: Argon2idOptions): Uint8Array`
Derives a key from a password using the Argon2id algorithm.
| Parameter | Type | Description |
|-----------|------|-------------|
| `password` | `Uint8Array` | Password bytes. |
| `options.salt` | `Uint8Array` | Random salt (≥16 bytes per RFC 9106). |
| `options.timeCost` | `number` | Number of passes over memory (≥1). |
| `options.memoryCost` | `number` | Memory usage in KiB (≥8 × parallelism). |
| `options.parallelism` | `number` | Degree of parallelism (≥1). |
| `options.length` | `number` | Output length in bytes (≥4). |
**Returns:** Derived key of `options.length` bytes.
### `Argon2idOptions`
```ts
interface Argon2idOptions {
readonly salt: Uint8Array;
readonly timeCost: number;
readonly memoryCost: number;
readonly parallelism: number;
readonly length: number;
}
```
## Implementation Notes
Noble's Argon2id implementation is a pure-JavaScript port that runs synchronously. It passes RFC 9106 Appendix B test vectors. Memory consumption scales linearly with `memoryCost` in KiB, so high memory costs can cause out-of-memory errors in constrained environments (e.g. edge workers). The primitive does not enforce RFC 9106 parameter minima — those checks are delegated to the public KDF façade. The native stub throws `PrimitiveNotImplemented("argon2id")`. Unlike PBKDF2, Argon2id is inherently sequential in its memory access, which limits parallelism-based GPU attacks but also means JavaScript's single-threaded nature does not significantly disadvantage it relative to native binaries.

View File

@@ -0,0 +1,83 @@
# argon2id.native
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`argon2id.native.ts` is the native-backend placeholder for the Argon2id primitive in `@suaveplan/crypto-hash`. It exports the same `argon2id` function and `Argon2idOptions` interface as the Noble implementation but throws `PrimitiveNotImplemented("argon2id")` unconditionally.
This stub completes the two-implementation selector layout for Argon2id. Argon2id's native backend is the most valuable of the placeholders because a native implementation could run Argon2id in a separate thread and avoid blocking the event loop, which the pure-JavaScript Noble implementation cannot do. The stub communicates that this capability is planned but not yet available.
## Features
- Satisfies `argon2id(password, options): Uint8Array` interface.
- Exports `Argon2idOptions` (identical to Noble file) for selector re-export.
- Throws `PrimitiveNotImplemented("argon2id")` unconditionally.
- Zero runtime dependencies beyond `@suaveplan/crypto-utils/errors`.
## Usage: Basic Example
```ts
// Active only when crypto.config.ts sets argon2id to 'native'.
import { argon2id } from "@suaveplan/crypto-hash/primitives/argon2id/argon2id";
try {
argon2id(new Uint8Array(8), {
salt: new Uint8Array(16),
timeCost: 1,
memoryCost: 8192,
parallelism: 1,
length: 32,
});
} catch (err) {
// PrimitiveNotImplemented: argon2id
console.error(err);
}
```
## Usage: Advanced Example
```ts
import type { Argon2idOptions } from "@suaveplan/crypto-hash/primitives/argon2id/argon2id";
// Build a typed options object to pass to whichever backend is active.
function interactiveParams(salt: Uint8Array): Argon2idOptions {
return {
salt,
timeCost: 2,
memoryCost: 19456,
parallelism: 1,
length: 32,
};
}
```
## API Reference
### `argon2id(_password: Uint8Array, _options: Argon2idOptions): Uint8Array`
Placeholder that always throws.
| Parameter | Type | Description |
|-----------|------|-------------|
| `_password` | `Uint8Array` | Ignored. |
| `_options` | `Argon2idOptions` | Ignored. |
**Throws:** `PrimitiveNotImplemented("argon2id")` — always.
### `Argon2idOptions`
```ts
interface Argon2idOptions {
readonly salt: Uint8Array;
readonly timeCost: number;
readonly memoryCost: number;
readonly parallelism: number;
readonly length: number;
}
```
## Implementation Notes
The future native implementation is expected to call into `libsodium` or a dedicated Argon2 native module to run the memory-hard computation off the JavaScript event loop. When that implementation lands and passes RFC 9106 Appendix B test vectors via the KAT harness, this file is updated, parameter prefixes removed, and the selector regenerated to point here. The `Argon2idOptions` interface is self-contained in this file so both backends are independent — the selector re-exports from whichever is active.

View File

@@ -0,0 +1,88 @@
# argon2id.noble
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`argon2id.noble.ts` is the Noble-backed concrete implementation of Argon2id in `@suaveplan/crypto-hash`. It wraps `argon2id` from `@noble/hashes/argon2`, translating the `Argon2idOptions` struct into Noble's parameter object (`t`, `m`, `p`, `dkLen`).
Argon2id (RFC 9106) is the Password Hashing Competition winner, combining the data-dependent memory access of Argon2d with the data-independent pass structure of Argon2i. It is the recommended algorithm when deriving keys from passwords or other low-entropy secrets. This file is the production implementation; `argon2id.native.ts` is the placeholder stub.
## Features
- Accepts `Argon2idOptions` with `salt`, `timeCost`, `memoryCost`, `parallelism`, and `length`.
- Delegates to `@noble/hashes/argon2` which implements the full RFC 9106 algorithm.
- Runs synchronously (no worker/thread offload at this layer).
- Exports `Argon2idOptions` for re-export by the selector.
- RFC 9106 Appendix B test vectors pass in the test suite.
## Usage: Basic Example
```ts
import { argon2id } from "@suaveplan/crypto-hash/primitives/argon2id/argon2id";
const password = new TextEncoder().encode("correct-horse-battery-staple");
const salt = crypto.getRandomValues(new Uint8Array(16));
const key = argon2id(password, {
salt,
timeCost: 3,
memoryCost: 65536,
parallelism: 4,
length: 32,
});
console.log(key.length); // 32
```
## Usage: Advanced Example
```ts
import { argon2id, type Argon2idOptions } from "@suaveplan/crypto-hash/primitives/argon2id/argon2id";
// Server-side registration: hash the password and store only the derived key.
const SERVER_PARAMS: Omit<Argon2idOptions, "salt"> = {
timeCost: 3,
memoryCost: 65536,
parallelism: 4,
length: 64,
};
function hashPassword(password: string): { salt: Uint8Array; key: Uint8Array } {
const salt = crypto.getRandomValues(new Uint8Array(16));
const key = argon2id(new TextEncoder().encode(password), { ...SERVER_PARAMS, salt });
return { salt, key };
}
```
## API Reference
### `argon2id(password: Uint8Array, options: Argon2idOptions): Uint8Array`
Derives key material from a password using Argon2id.
| Parameter | Type | Description |
|-----------|------|-------------|
| `password` | `Uint8Array` | Password bytes. |
| `options.salt` | `Uint8Array` | Random salt (≥16 bytes per RFC 9106). |
| `options.timeCost` | `number` | Number of passes over the memory array (≥1). |
| `options.memoryCost` | `number` | Memory usage in KiB (≥8 × parallelism). |
| `options.parallelism` | `number` | Degree of parallelism (≥1). |
| `options.length` | `number` | Derived key length in bytes (≥4). |
**Returns:** `Uint8Array` of `options.length` bytes.
### `Argon2idOptions`
```ts
interface Argon2idOptions {
readonly salt: Uint8Array;
readonly timeCost: number;
readonly memoryCost: number;
readonly parallelism: number;
readonly length: number;
}
```
## Implementation Notes
Noble maps `timeCost → t`, `memoryCost → m`, `parallelism → p`, and `length → dkLen`. All parameter validation (RFC 9106 minima checks) is delegated to the public KDF façade; this file passes values through without checking. Noble's Argon2id is a pure-JavaScript implementation — no WASM — so it runs in any JavaScript environment but may be slower than a native binary for high memory costs. Synchronous execution on large `memoryCost` values can block the event loop; callers that need async should wrap in a worker thread.

Some files were not shown because too many files have changed in this diff Show More