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,21 @@
# @suaveplan/crypto-sodium — Changelog
## 0.0.0 (suaveplan)
Initial implementation on the suaveplan branch.
### Added
- `sodiumReady(): Promise<void>` — lazy WASM initialisation singleton.
- `sodiumAeadEngine` — XChaCha20-Poly1305 / ChaCha20-Poly1305 / AES-256-GCM AEAD engine.
- `sodiumAesGcmEngine` — AES-256-GCM–preferring AEAD engine (falls back to ChaCha20).
- `createSodiumAeadEngine(opts)` — factory for custom AEAD engine configurations.
- `sodiumSignEngine` — Ed25519 sign/verify/generateKeypair engine.
- `sodiumAgreeEngine` — X25519 Diffie-Hellman agree/generateKeypair engine.
- `sodiumSealedBoxEngine` — anonymous-sender sealed box (`crypto_box_seal`).
- `sodiumAuthBoxEngine` — authenticated box (`crypto_box_easy`).
- `cryptoKdf` — BLAKE2b-based deterministic subkey derivation.
- `cryptoAuth` / `cryptoAuthVerify` — BLAKE2b MAC.
- `cryptoPwhashStr` / `cryptoPwhashStrVerify` — Argon2id password hashing.
- `ristretto255` namespace — group operations (add, sub, mul, fromHash, toBytes, fromBytes, random).
- `SodiumError` + `SODIUM_CODES` — structured error surface.

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,96 @@
# engines/sodium-aead-engine
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`sodiumAeadEngine` is a libsodium-backed implementation of the `AeadEngine` interface from `@suaveplan/crypto`. It provides authenticated encryption with associated data (AEAD) via three algorithms selectable by nonce length: XChaCha20-Poly1305 (24-byte nonce), ChaCha20-Poly1305 IETF (12-byte nonce), and AES-256-GCM (12-byte nonce, hardware only).
The engine is a drop-in for the noble-based default engine in `@suaveplan/crypto`. It uses libsodium's WASM implementation, which uses the platform's native AES-GCM hardware acceleration when available and detectable via `sodium.crypto_aead_aes256gcm_is_available()`.
## Features
- XChaCha20-Poly1305 IETF via `crypto_aead_xchacha20poly1305_ietf_*` — 24-byte nonce, resistant to nonce-reuse compared to 12-byte variants.
- ChaCha20-Poly1305 IETF via `crypto_aead_chacha20poly1305_ietf_*` — 12-byte nonce, default for 12-byte nonce paths.
- AES-256-GCM via `crypto_aead_aes256gcm_*` — hardware-accelerated path enabled with `preferAes: true`.
- Associated data (AAD) support — empty AAD is used when `aad` is omitted.
- Throws `SodiumError` with code `SODIUM.INIT.FAILED` on authentication tag mismatch during decryption.
- Factory function `createSodiumAeadEngine` for custom configurations; singleton exports for convenience.
## Usage: Basic Example
```ts
import { sodiumReady } from "@suaveplan/crypto-sodium/init";
import { sodiumAeadEngine } from "@suaveplan/crypto-sodium/engines/aead";
await sodiumReady();
const key = crypto.getRandomValues(new Uint8Array(32));
const nonce = crypto.getRandomValues(new Uint8Array(24)); // XChaCha20
const plaintext = new TextEncoder().encode("secret message");
const ciphertext = sodiumAeadEngine.encrypt(key, plaintext, nonce);
const recovered = sodiumAeadEngine.decrypt(key, ciphertext, nonce);
console.log(new TextDecoder().decode(recovered)); // "secret message"
```
## Usage: Advanced Example
Using AAD and the AES-256-GCM–preferring engine:
```ts
import { sodiumReady } from "@suaveplan/crypto-sodium/init";
import {
sodiumAesGcmEngine,
createSodiumAeadEngine,
} from "@suaveplan/crypto-sodium/engines/aead";
await sodiumReady();
const key = crypto.getRandomValues(new Uint8Array(32));
const nonce = crypto.getRandomValues(new Uint8Array(12)); // ChaCha20 or AES-GCM
const plaintext = new TextEncoder().encode("data");
const aad = new TextEncoder().encode("header-v1");
// Prefer AES-GCM when hardware is available, fall back to ChaCha20
const ct = sodiumAesGcmEngine.encrypt(key, plaintext, nonce, aad);
const pt = sodiumAesGcmEngine.decrypt(key, ct, nonce, aad);
// Or create a custom engine:
const customEngine = createSodiumAeadEngine({ preferAes: false });
```
## API Reference
### `sodiumAeadEngine: AeadEngine`
Default engine using XChaCha20-Poly1305 (24-byte nonce) and ChaCha20-Poly1305 IETF (12-byte nonce). `preferAes` is `false`.
### `sodiumAesGcmEngine: AeadEngine`
Engine that prefers AES-256-GCM for 12-byte nonces when the platform supports it; falls back to ChaCha20-Poly1305.
### `createSodiumAeadEngine(options?: SodiumAeadOptions): AeadEngine`
Creates a configured engine.
**`SodiumAeadOptions`:**
- `preferAes?: boolean` — default `false`. When `true`, routes 12-byte nonces through `crypto_aead_aes256gcm_*` if hardware is available.
### `AeadEngine.encrypt(key, plaintext, nonce, aad?): Uint8Array`
Encrypts `plaintext` and returns `ciphertext || tag`.
### `AeadEngine.decrypt(key, ciphertext, nonce, aad?): Uint8Array`
Decrypts and authenticates. Throws `SodiumError` (`SODIUM.INIT.FAILED`) if authentication fails.
## Implementation Notes
Algorithm routing is determined by nonce length at call time, not at engine-creation time. A 24-byte nonce always routes to XChaCha20-Poly1305; a 12-byte nonce routes to AES-256-GCM (when `preferAes` and available) or ChaCha20-Poly1305. This mirrors how `@suaveplan/crypto`'s `aeadEncrypt` picks algorithms.
libsodium WASM does not expose software AES-GCM — `crypto_aead_aes256gcm_is_available()` returns `false` on platforms without hardware AES-NI. For maximum portability, leave `preferAes: false` and use XChaCha20-Poly1305 with 24-byte nonces.
The `assertReady()` guard checks a sodium constant (`crypto_aead_xchacha20poly1305_ietf_KEYBYTES`) synchronously. If undefined, the WASM has not initialised yet — callers must await `sodiumReady()` first.

View File

@@ -0,0 +1,81 @@
# engines/sodium-agree-engine
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`sodiumAgreeEngine` implements the `AgreeEngine` interface from `@suaveplan/crypto` using libsodium's X25519 Diffie-Hellman key agreement. X25519 is the modern industry-standard ECDH scheme used in TLS 1.3, Signal, Noise Protocol, and WireGuard.
The engine produces a raw 32-byte shared secret from two Curve25519 keypairs. Callers are responsible for deriving a symmetric key from this shared secret — use `cryptoKdf` or a KDF such as HKDF for this purpose.
## Features
- X25519 keypair generation via `crypto_kx_keypair` — Curve25519 points compatible with `crypto_scalarmult`.
- Diffie-Hellman shared secret via `crypto_scalarmult` — raw X25519 output, 32 bytes.
- Conforms to `AgreeEngine` from `@suaveplan/crypto/engines` — pluggable into any higher-level key-exchange protocol.
- `sodiumReady()` must be awaited before first use.
## Usage: Basic Example
```ts
import { sodiumReady } from "@suaveplan/crypto-sodium/init";
import { sodiumAgreeEngine } from "@suaveplan/crypto-sodium/engines/agree";
await sodiumReady();
const alice = sodiumAgreeEngine.generateKeypair();
const bob = sodiumAgreeEngine.generateKeypair();
const aliceShared = sodiumAgreeEngine.agree(alice.secretKey, bob.publicKey);
const bobShared = sodiumAgreeEngine.agree(bob.secretKey, alice.publicKey);
// aliceShared deepEquals bobShared — both parties derived the same secret
```
## Usage: Advanced Example
Combining X25519 agree with KDF for a symmetric key:
```ts
import { sodiumReady } from "@suaveplan/crypto-sodium/init";
import { sodiumAgreeEngine } from "@suaveplan/crypto-sodium/engines/agree";
import { cryptoKdf } from "@suaveplan/crypto-sodium/primitives/kdf";
await sodiumReady();
const alice = sodiumAgreeEngine.generateKeypair();
const bob = sodiumAgreeEngine.generateKeypair();
// Raw shared secret — not safe to use directly as a key
const rawSecret = sodiumAgreeEngine.agree(alice.secretKey, bob.publicKey);
// Derive a 32-byte encryption key via KDF
const encKey = await cryptoKdf(rawSecret, 1, "EncryptN", 32);
```
## API Reference
### `sodiumAgreeEngine: AgreeEngine`
Singleton engine object.
### `generateKeypair(byteSource?: ByteSource): AgreeKeypair`
Generates a fresh Curve25519 keypair. `byteSource` is accepted for interface compatibility but ignored.
**Returns:** `{ publicKey: Uint8Array (32 bytes), secretKey: Uint8Array (32 bytes) }`
### `agree(mySecret: Uint8Array, theirPublic: Uint8Array): Uint8Array`
Performs X25519 Diffie-Hellman.
**Returns:** 32-byte raw shared secret.
## Implementation Notes
`crypto_kx_keypair` produces Curve25519 keypairs that are fully compatible with `crypto_scalarmult`. The `crypto_kx_*` family includes session-key derivation helpers, but this engine uses `crypto_scalarmult` directly to return the raw shared secret, giving callers full control over key derivation.
The raw X25519 output is a group element, not a uniformly random key. It MUST be passed through a KDF before use in a symmetric cipher. See `cryptoKdf` in `@suaveplan/crypto-sodium/primitives/kdf` for the libsodium-native option.
`byteSource` is not forwarded to libsodium for the same reason as in `sodiumSignEngine` — libsodium's WASM manages entropy internally.

View File

@@ -0,0 +1,110 @@
# engines/sodium-auth-box-engine
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`sodiumAuthBoxEngine` implements the `AuthBoxEngine` interface from `@suaveplan/crypto` using libsodium's `crypto_box_easy` and `crypto_box_open_easy`. This construction provides authenticated key-exchange encryption: both the sender and recipient are authenticated because the shared secret is derived from a static X25519 DH between sender's secret and recipient's public key, then used to encrypt with XSalsa20-Poly1305.
Unlike `sodiumSealedBoxEngine`, the sender's public key is not included in the ciphertext — the recipient must already know who sent the message (i.e., possess the sender's public key independently).
## Features
- Mutually authenticated encryption using `crypto_box_easy` / `crypto_box_open_easy`.
- XSalsa20-Poly1305 authenticated encryption via X25519 key agreement.
- 24-byte explicit nonce required from caller.
- 16-byte MAC overhead in the ciphertext.
- Throws `SodiumError` on authentication failure during `open`.
- Conforms to `AuthBoxEngine` from `@suaveplan/crypto/engines`.
## Usage: Basic Example
```ts
import { sodiumReady } from "@suaveplan/crypto-sodium/init";
import { sodiumAgreeEngine } from "@suaveplan/crypto-sodium/engines/agree";
import { sodiumAuthBoxEngine } from "@suaveplan/crypto-sodium/engines/auth-box";
await sodiumReady();
const alice = sodiumAgreeEngine.generateKeypair();
const bob = sodiumAgreeEngine.generateKeypair();
const nonce = crypto.getRandomValues(new Uint8Array(24));
const msg = new TextEncoder().encode("authenticated message");
// Alice sends to Bob
const ct = sodiumAuthBoxEngine.seal(
alice.secretKey,
bob.publicKey,
msg,
nonce
);
// Bob receives from Alice
const pt = sodiumAuthBoxEngine.open(
alice.publicKey,
bob.secretKey,
ct,
nonce
);
console.log(new TextDecoder().decode(pt)); // "authenticated message"
```
## Usage: Advanced Example
Bidirectional messaging channel with distinct nonce counters per direction:
```ts
import { sodiumReady } from "@suaveplan/crypto-sodium/init";
import { sodiumAgreeEngine } from "@suaveplan/crypto-sodium/engines/agree";
import { sodiumAuthBoxEngine } from "@suaveplan/crypto-sodium/engines/auth-box";
await sodiumReady();
const alice = sodiumAgreeEngine.generateKeypair();
const bob = sodiumAgreeEngine.generateKeypair();
function incrementNonce(nonce: Uint8Array): Uint8Array {
const n = nonce.slice();
for (let i = n.length - 1; i >= 0; i--) {
if (++n[i]) break;
}
return n;
}
let aliceNonce = new Uint8Array(24);
const ct1 = sodiumAuthBoxEngine.seal(
alice.secretKey, bob.publicKey,
new TextEncoder().encode("msg 1"), aliceNonce
);
aliceNonce = incrementNonce(aliceNonce);
```
## API Reference
### `sodiumAuthBoxEngine: AuthBoxEngine`
Singleton engine.
### `seal(senderSecret, recipientPublic, plaintext, nonce): Uint8Array`
Encrypts `plaintext` using `crypto_box_easy`. Returns ciphertext with 16-byte MAC prepended.
**Parameters:**
- `senderSecret` — sender's 32-byte secret key.
- `recipientPublic` — recipient's 32-byte public key.
- `plaintext` — message bytes.
- `nonce` — 24-byte nonce (must be unique per message under the same keypair).
### `open(senderPublic, recipientSecret, ciphertext, nonce): Uint8Array`
Decrypts and authenticates. Both keys must correspond to the same exchange as `seal`.
**Throws:** `SodiumError` (`SODIUM.INIT.FAILED`) if MAC verification fails.
## Implementation Notes
`crypto_box_easy` computes `Curve25519(senderSecret, recipientPublic)` as the shared secret and uses HSalsa20 as KDF before XSalsa20-Poly1305. Nonces must be 24 bytes. The nonce management strategy (random, counter, or derived) is left to the caller — each (keypair, nonce) pair must be used at most once for confidentiality.
The MAC overhead is 16 bytes (`crypto_box_MACBYTES`). The nonce must be transmitted alongside the ciphertext; a common convention is to prepend it to the ciphertext.

View File

@@ -0,0 +1,98 @@
# engines/sodium-sealed-box-engine
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`sodiumSealedBoxEngine` implements the `SealedBoxEngine` interface from `@suaveplan/crypto` using libsodium's `crypto_box_seal` and `crypto_box_seal_open`. This construction provides anonymous-sender encryption: the sender generates an ephemeral X25519 keypair, performs a Diffie-Hellman with the recipient's public key, derives a shared secret, and encrypts with XSalsa20-Poly1305. The ephemeral keypair is discarded after encryption.
Because the sender's identity is ephemeral and included only implicitly in the ciphertext, the recipient cannot determine who sent the message. Use `sodiumAuthBoxEngine` when sender authentication is required.
## Features
- Anonymous-sender AEAD using `crypto_box_seal` / `crypto_box_seal_open`.
- Ephemeral X25519 key exchange — sender identity is not revealed.
- XSalsa20-Poly1305 authenticated encryption under the hood.
- Ciphertext overhead: 48 bytes (32 ephemeral public key + 16 MAC).
- Throws `SodiumError` on authentication failure during `open`.
- Conforms to `SealedBoxEngine` from `@suaveplan/crypto/engines`.
## Usage: Basic Example
```ts
import { sodiumReady } from "@suaveplan/crypto-sodium/init";
import { sodiumAgreeEngine } from "@suaveplan/crypto-sodium/engines/agree";
import { sodiumSealedBoxEngine } from "@suaveplan/crypto-sodium/engines/sealed-box";
await sodiumReady();
const recipientKp = sodiumAgreeEngine.generateKeypair();
const msg = new TextEncoder().encode("anonymous message");
const ct = sodiumSealedBoxEngine.seal(recipientKp.publicKey, msg);
const pt = sodiumSealedBoxEngine.open(
recipientKp.secretKey,
recipientKp.publicKey,
ct
);
console.log(new TextDecoder().decode(pt)); // "anonymous message"
```
## Usage: Advanced Example
Sealed-box used as an encrypted submission envelope:
```ts
import { sodiumReady } from "@suaveplan/crypto-sodium/init";
import { sodiumAgreeEngine } from "@suaveplan/crypto-sodium/engines/agree";
import { sodiumSealedBoxEngine } from "@suaveplan/crypto-sodium/engines/sealed-box";
import { SodiumError } from "@suaveplan/crypto-sodium/errors";
await sodiumReady();
const serverKp = sodiumAgreeEngine.generateKeypair();
// Client submits encrypted payload — server public key is published
function clientSubmit(serverPub: Uint8Array, data: Uint8Array): Uint8Array {
return sodiumSealedBoxEngine.seal(serverPub, data);
}
// Server decrypts — only it holds the secret key
function serverReceive(ct: Uint8Array): Uint8Array {
try {
return sodiumSealedBoxEngine.open(
serverKp.secretKey,
serverKp.publicKey,
ct
);
} catch (err) {
if (err instanceof SodiumError) {
throw new Error("Tampered submission rejected");
}
throw err;
}
}
```
## API Reference
### `sodiumSealedBoxEngine: SealedBoxEngine`
Singleton engine.
### `seal(recipientPublic: Uint8Array, plaintext: Uint8Array): Uint8Array`
Encrypts `plaintext` to the recipient's public key. Returns `ephemeralPublicKey (32 bytes) || ciphertext+MAC`.
### `open(recipientSecret: Uint8Array, recipientPublic: Uint8Array, ciphertext: Uint8Array): Uint8Array`
Opens a sealed box. Both `recipientSecret` and `recipientPublic` are required because libsodium uses both for the Diffie-Hellman computation.
**Throws:** `SodiumError` (`SODIUM.INIT.FAILED`) if the MAC verification fails.
## Implementation Notes
libsodium's `crypto_box_seal_open` requires both keys to reconstruct the X25519 shared secret from the ephemeral public key embedded in the ciphertext. The function throws natively on authentication failure; this engine catches that and wraps it in a `SodiumError` for uniform error handling.
The ciphertext overhead is always exactly 48 bytes: 32 bytes for the sender's ephemeral public key prepended to the ciphertext, and 16 bytes for the Poly1305 authentication tag.

View File

@@ -0,0 +1,88 @@
# engines/sodium-sign-engine
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`sodiumSignEngine` implements the `SignEngine` interface from `@suaveplan/crypto` using libsodium's Ed25519 signing primitives. Ed25519 is a deterministic signature scheme based on the Edwards-curve digital signature algorithm — it provides strong security guarantees, fast verification, and small key and signature sizes.
This engine replaces the noble-curves default in performance-sensitive or server-side scenarios where the libsodium WASM path is preferred for its maturity and NIST/IETF certification heritage.
## Features
- Ed25519 keypair generation via `crypto_sign_keypair` — uses libsodium's internal secure entropy.
- Detached signature creation via `crypto_sign_detached` — signature is separate from the message.
- Detached signature verification via `crypto_sign_verify_detached` — returns boolean, never throws on invalid signatures.
- 32-byte public keys, 64-byte secret keys (includes seed), 64-byte signatures.
- Conforms to `SignEngine` from `@suaveplan/crypto/engines` — drop-in for the noble default.
## Usage: Basic Example
```ts
import { sodiumReady } from "@suaveplan/crypto-sodium/init";
import { sodiumSignEngine } from "@suaveplan/crypto-sodium/engines/sign";
await sodiumReady();
const kp = sodiumSignEngine.generateKeypair();
const msg = new TextEncoder().encode("sign this");
const sig = sodiumSignEngine.sign(kp.secretKey, msg);
const valid = sodiumSignEngine.verify(kp.publicKey, sig, msg);
console.log(valid); // true
```
## Usage: Advanced Example
Using `sodiumSignEngine` as a pluggable engine with `@suaveplan/crypto`:
```ts
import { sodiumReady } from "@suaveplan/crypto-sodium/init";
import { sodiumSignEngine } from "@suaveplan/crypto-sodium/engines/sign";
import type { SignEngine } from "@suaveplan/crypto/engines";
await sodiumReady();
function signDocument(
engine: SignEngine,
secretKey: Uint8Array,
document: Uint8Array
): Uint8Array {
return engine.sign(secretKey, document);
}
const kp = sodiumSignEngine.generateKeypair();
const doc = new TextEncoder().encode("legal document text");
const sig = signDocument(sodiumSignEngine, kp.secretKey, doc);
// Verify on the receiving side:
const ok = sodiumSignEngine.verify(kp.publicKey, sig, doc);
```
## API Reference
### `sodiumSignEngine: SignEngine`
Singleton engine object.
### `generateKeypair(byteSource?: ByteSource): SignKeypair`
Generates a fresh Ed25519 keypair. The `byteSource` parameter is accepted for interface compatibility but ignored — libsodium manages its own CSPRNG.
**Returns:** `{ publicKey: Uint8Array (32 bytes), secretKey: Uint8Array (64 bytes) }`
### `sign(secretKey: Uint8Array, message: Uint8Array): Uint8Array`
Produces a 64-byte detached Ed25519 signature over `message`.
### `verify(publicKey: Uint8Array, signature: Uint8Array, message: Uint8Array): boolean`
Verifies a detached signature. Returns `false` rather than throwing on any verification failure (wrong key, tampered sig, wrong message). Exceptions from libsodium are caught and mapped to `false`.
## Implementation Notes
libsodium's `crypto_sign_keypair()` returns `{ publicKey, privateKey }` — note the property is named `privateKey` in the TS bindings despite being the full 64-byte secret key (seed + public key concatenated). The engine remaps this to `secretKey` to match the `SignKeypair` interface.
The `byteSource` parameter is not forwarded to libsodium because libsodium's WASM module manages entropy internally. Callers cannot substitute an alternative random source for key generation via this engine.
`verify` wraps `crypto_sign_verify_detached` in a try/catch because libsodium throws for structurally malformed inputs (e.g., a signature buffer that is not 64 bytes), mapping those to `false` for consistency with the `SignEngine` contract.

View File

@@ -0,0 +1,80 @@
# SODIUM_CODES
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`codes.ts` defines the complete set of stable error codes used throughout `@suaveplan/crypto-sodium`. Each code is a namespaced string constant produced by `defineCodes` from `@suaveplan/error`, ensuring that codes are unique within the `"SODIUM"` namespace and that the `SodiumCode` union type remains in sync with the registered values at compile time. Consumers can branch on `error.code` without inspecting message strings, making error handling robust across version upgrades.
## Features
- Four codes covering the four libsodium-specific failure domains: library initialisation, KDF context/length validation, and HMAC authentication verification
- Generated via `defineCodes("SODIUM", [...])` which prefixes each entry and freezes the resulting object
- `SodiumCode` is derived as a `typeof` union so TypeScript narrows exhaustively in switch statements
- No runtime dependencies beyond `@suaveplan/error`
## Usage: Basic Example
```typescript
import { SODIUM_CODES } from "@suaveplan/crypto-sodium/errors/codes";
import { SodiumError } from "@suaveplan/crypto-sodium/errors/sodium-error";
throw new SodiumError(
SODIUM_CODES["INIT.FAILED"],
"error.crypto-sodium.init.failed"
);
```
## Usage: Advanced Example
```typescript
import { SODIUM_CODES, type SodiumCode } from "@suaveplan/crypto-sodium/errors/codes";
import { SodiumError } from "@suaveplan/crypto-sodium/errors/sodium-error";
function handleSodiumError(err: SodiumError): string {
switch (err.code as SodiumCode) {
case SODIUM_CODES["INIT.FAILED"]:
return "libsodium failed to initialise — WebAssembly may not be available.";
case SODIUM_CODES["KDF.INVALID_CONTEXT"]:
return "KDF context must be exactly 8 bytes.";
case SODIUM_CODES["KDF.INVALID_LENGTH"]:
return "Derived key length is outside the range permitted by crypto_kdf.";
case SODIUM_CODES["AUTH.VERIFY_FAILED"]:
return "HMAC authentication tag did not match — message rejected.";
default:
return "Unknown sodium error.";
}
}
```
## API Reference
### `SODIUM_CODES`
```typescript
const SODIUM_CODES: Readonly<Record<string, string>>
```
A frozen map from short code keys to their namespaced string values. All values are prefixed with `"SODIUM."` by `defineCodes`.
| Key | Namespaced value | When thrown |
|----------------------|---------------------------------|----------------------------------------------------------|
| `INIT.FAILED` | `"SODIUM.INIT.FAILED"` | libsodium initialisation (WASM load) failed |
| `KDF.INVALID_CONTEXT`| `"SODIUM.KDF.INVALID_CONTEXT"` | `crypto_kdf` context string is not exactly 8 bytes |
| `KDF.INVALID_LENGTH` | `"SODIUM.KDF.INVALID_LENGTH"` | Requested derived key length is out of range for `crypto_kdf` |
| `AUTH.VERIFY_FAILED` | `"SODIUM.AUTH.VERIFY_FAILED"` | `crypto_auth_verify` returned a failure |
### `SodiumCode`
```typescript
type SodiumCode = (typeof SODIUM_CODES)[keyof typeof SODIUM_CODES];
```
Union of all namespaced code strings. Use this type to annotate `err.code` when narrowing `SodiumError` in typed catch blocks.
## Implementation Notes
`defineCodes` returns a plain frozen object. The `SodiumCode` type is derived with `typeof SODIUM_CODES[keyof typeof SODIUM_CODES]`, which collapses to a union of string literals at compile time. This approach means adding a new code to the array automatically widens the union, and any exhaustive switch statement will produce a TypeScript error if the new case is not handled.
The `"AUTH.VERIFY_FAILED"` code covers `crypto_auth` HMAC-SHA512256 verification failures only. AEAD decryption failures in the sodium AEAD engine re-use the `AEAD.DECRYPT_FAILED` code from `@suaveplan/crypto` (`CryptoError`) because those errors flow through the shared `AeadEngine` interface and consumers expect that specific code.

View File

@@ -0,0 +1,94 @@
# errors — SodiumError
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`SodiumError` is the structured error class for all failures originating in `@suaveplan/crypto-sodium`. It extends `SuaveplanError<SodiumCode>` from `@suaveplan/error`, providing a stable, machine-readable `code` field alongside an optional `cause` chain and `context` map for structured logging, observability, and serialisation.
Every `throw` in this package produces a `SodiumError` instance, enabling callers to narrow error handling with `instanceof SodiumError` or by inspecting `error.code` against the exported `SODIUM_CODES` registry.
## Features
- Extends `SuaveplanError<SodiumCode>` — participates in the full `@suaveplan/error` ecosystem (cause-chain traversal, OTel span events, wire serialisation).
- Stable code registry via `SODIUM_CODES` — codes are defined with `defineCodes("SODIUM", [...])` and never change between package versions.
- Full `context` support — structured fields attached at throw site for log enrichment without leaking sensitive bytes.
- `cause` preservation — wraps underlying libsodium errors for root-cause analysis.
## Usage: Basic Example
```ts
import { SodiumError, SODIUM_CODES } from "@suaveplan/crypto-sodium/errors";
try {
// ...some crypto-sodium operation
} catch (err) {
if (err instanceof SodiumError) {
console.error("sodium failure:", err.code, err.message);
}
}
```
## Usage: Advanced Example
Matching against specific codes and extracting context:
```ts
import { SodiumError, SODIUM_CODES } from "@suaveplan/crypto-sodium/errors";
import { walkCauses } from "@suaveplan/error";
function handleError(err: unknown): void {
if (!(err instanceof SodiumError)) throw err;
switch (err.code) {
case SODIUM_CODES["KDF.INVALID_CONTEXT"]:
console.warn("KDF context must be exactly 8 bytes", err.context);
break;
case SODIUM_CODES["KDF.INVALID_LENGTH"]:
console.warn("KDF length out of range", err.context);
break;
case SODIUM_CODES["INIT.FAILED"]:
// Inspect the original libsodium error
for (const cause of walkCauses(err)) {
console.error(" caused by:", cause);
}
break;
default:
throw err;
}
}
```
## API Reference
### `class SodiumError extends SuaveplanError<SodiumCode>`
**Constructor:** `new SodiumError(code: SodiumCode, messageKey: string, options?: SuaveplanErrorOptions)`
- `code` — one of `SODIUM_CODES[keyof typeof SODIUM_CODES]`.
- `messageKey` — i18n resolver key, falls back to the key string if no resolver is registered.
- `options.cause` — underlying error to preserve in the cause chain.
- `options.context` — structured fields for logging; redacted at the wire boundary.
- `options.traceId` — optional trace correlation id.
### `SODIUM_CODES`
Stable code registry produced by `defineCodes("SODIUM", [...])`:
| Key | Code string |
|-----|-------------|
| `INIT.FAILED` | `SODIUM.INIT.FAILED` |
| `KDF.INVALID_CONTEXT` | `SODIUM.KDF.INVALID_CONTEXT` |
| `KDF.INVALID_LENGTH` | `SODIUM.KDF.INVALID_LENGTH` |
| `AUTH.VERIFY_FAILED` | `SODIUM.AUTH.VERIFY_FAILED` |
### `type SodiumCode`
Union of all values in `SODIUM_CODES`. Use for typed error-code comparisons.
## Implementation Notes
`SodiumError` is a thin subclass — its sole purpose is widening the `protected` `SuaveplanError` constructor to `public` so call sites can instantiate it directly. The `// biome-ignore` comment on the constructor is intentional; Biome's `noUselessConstructor` rule would otherwise flag the pass-through, but the accessibility change is the actual purpose.
Codes are frozen at `defineCodes` time, making them suitable for storage in databases or event streams where code strings must remain stable across deployments.

View File

@@ -0,0 +1,70 @@
# init — sodiumReady
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`sodiumReady` is the initialisation gateway for the `libsodium-wrappers-sumo` WASM module. libsodium ships as WebAssembly, which requires an asynchronous compilation and instantiation step before any cryptographic API becomes available. `sodiumReady` wraps that step in an idempotent singleton Promise so the WASM is compiled exactly once regardless of how many concurrent callers exist.
All engine implementations (`sodiumAeadEngine`, `sodiumSignEngine`, etc.) call `sodiumReady()` internally where they are async. The synchronous engines require `sodiumReady()` to have been awaited before first use — typically in a `beforeAll` hook or an application startup sequence.
## Features
- Single lazy initialisation — WASM is loaded only when first requested.
- Idempotent — every call after the first returns the same settled Promise at zero cost.
- Error recovery — if initialisation fails, the internal promise reference is cleared so a retry is possible on the next call.
- Throws `SodiumError` with code `SODIUM.INIT.FAILED` and the underlying cause preserved if libsodium's `ready` promise rejects.
## Usage: Basic Example
```ts
import { sodiumReady } from "@suaveplan/crypto-sodium/init";
import { sodiumSignEngine } from "@suaveplan/crypto-sodium/engines/sign";
async function main() {
await sodiumReady();
const kp = sodiumSignEngine.generateKeypair();
const sig = sodiumSignEngine.sign(kp.secretKey, new TextEncoder().encode("hello"));
console.log("signature bytes:", sig.length);
}
```
## Usage: Advanced Example
Pre-warming during application boot to avoid latency on the first real crypto call:
```ts
import { sodiumReady } from "@suaveplan/crypto-sodium/init";
// In your server bootstrap:
export async function bootstrap(): Promise<void> {
// Pre-warm WASM — all subsequent calls are synchronous no-ops.
await sodiumReady();
console.log("libsodium WASM ready");
}
// Concurrent callers share the same init — no double-load:
await Promise.all([sodiumReady(), sodiumReady(), sodiumReady()]);
```
## API Reference
### `sodiumReady(): Promise<void>`
Resolves when `libsodium-wrappers-sumo` WASM is fully initialised and all `sodium.*` APIs are callable synchronously.
**Returns:** `Promise<void>` — resolves to `undefined` on success.
**Throws:** `SodiumError` with code `SODIUM.INIT.FAILED` if libsodium's internal `ready` promise rejects.
**Idempotency:** The returned promise is the same object on every call after the first successful resolution; concurrent callers are coalesced into a single init sequence.
## Implementation Notes
libsodium's JS wrapper exports a `ready` property which is a `Promise<void>` that resolves once WASM compilation finishes. Before that promise settles, calling any sodium API throws a `ReferenceError`. The `sodiumReady` singleton ensures those calls only happen post-resolution.
The module-level `readyPromise` variable is `null` until the first call, preventing any eager WASM load on import. If the `ready` promise rejects (e.g., missing WASM in a restricted environment), `readyPromise` is reset to `null` so subsequent calls can retry — this is the only exception to idempotency.
Performance: after the first successful resolution, every subsequent `sodiumReady()` call resolves in a single microtask tick (a `.then()` on an already-resolved Promise), adding negligible overhead.

View File

@@ -0,0 +1,85 @@
# primitives/crypto-auth
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`cryptoAuth` and `cryptoAuthVerify` expose libsodium's `crypto_auth` and `crypto_auth_verify`, which implement a BLAKE2b-based message authentication code (MAC). Unlike HMAC-SHA256, this MAC is built on BLAKE2b — a fast, parallel, modern hash function with a wider security margin than HMAC-MD5 or HMAC-SHA1, and comparable to HMAC-SHA256/512.
MACs are used to verify that a message was produced by a party holding the shared key and has not been tampered with in transit. They differ from signatures in that both parties must hold the same secret key; there is no separate verification key.
## Features
- BLAKE2b-based MAC via `crypto_auth` — 32-byte output, 32-byte key.
- Constant-time verification via `crypto_auth_verify` — resistant to timing-based MAC forgery.
- `cryptoAuthVerify` returns `false` (never throws) for invalid MACs.
- Async — calls `sodiumReady()` internally.
- Deterministic — same key + message always produces the same MAC.
## Usage: Basic Example
```ts
import { cryptoAuth, cryptoAuthVerify } from "@suaveplan/crypto-sodium/primitives/auth";
const key = crypto.getRandomValues(new Uint8Array(32));
const message = new TextEncoder().encode("verify this");
const mac = await cryptoAuth(key, message);
const valid = await cryptoAuthVerify(key, message, mac);
console.log(valid); // true
```
## Usage: Advanced Example
Authenticating API request payloads with a shared secret:
```ts
import { cryptoAuth, cryptoAuthVerify } from "@suaveplan/crypto-sodium/primitives/auth";
const sharedKey = crypto.getRandomValues(new Uint8Array(32));
async function signPayload(payload: Uint8Array): Promise<{ payload: Uint8Array; mac: Uint8Array }> {
const mac = await cryptoAuth(sharedKey, payload);
return { payload, mac };
}
async function verifyPayload(payload: Uint8Array, mac: Uint8Array): Promise<boolean> {
return cryptoAuthVerify(sharedKey, payload, mac);
}
const data = new TextEncoder().encode('{"action":"transfer","amount":100}');
const signed = await signPayload(data);
const ok = await verifyPayload(signed.payload, signed.mac);
```
## API Reference
### `cryptoAuth(key: Uint8Array, message: Uint8Array): Promise<Uint8Array>`
Computes a 32-byte BLAKE2b MAC over `message` with `key`.
**Parameters:**
- `key` — 32-byte authentication key.
- `message` — Arbitrary-length message bytes.
**Returns:** `Promise<Uint8Array>` — 32-byte MAC.
### `cryptoAuthVerify(key: Uint8Array, message: Uint8Array, mac: Uint8Array): Promise<boolean>`
Verifies a MAC in constant time.
**Parameters:**
- `key` — 32-byte authentication key.
- `message` — The message that was authenticated.
- `mac` — 32-byte MAC to verify.
**Returns:** `Promise<boolean>` — `true` if valid, `false` otherwise. Never throws.
## Implementation Notes
`crypto_auth_verify` from libsodium uses constant-time comparison to prevent timing side channels that would otherwise allow an attacker to iteratively learn the correct MAC byte by byte. The `try/catch` in `cryptoAuthVerify` catches structurally malformed inputs (e.g., wrong-length MAC) and maps them to `false` for consistency.
Both functions are async solely because they call `sodiumReady()` — the underlying sodium operations are synchronous once the WASM is initialised.

View File

@@ -0,0 +1,87 @@
# primitives/crypto-kdf
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`cryptoKdf` provides deterministic subkey derivation via libsodium's `crypto_kdf_derive_from_key`, which implements BLAKE2b in KDF mode. Given a 32-byte master key, a numeric subkey identifier, an 8-byte application context string, and a desired length, it always produces the same output — making it suitable for generating multiple distinct keys (encryption, MAC, signing) from a single master without separate key storage.
The context string acts as a domain separator between applications or subsystems that share the same master key. Requiring exactly 8 bytes prevents both accidental collisions (empty context, overly short strings) and truncation attacks.
## Features
- Deterministic BLAKE2b-based KDF via `crypto_kdf_derive_from_key`.
- Context domain-separation — exactly 8 UTF-8 bytes required.
- Configurable output length: 16–64 bytes (inclusive).
- Different `subkeyId` values produce cryptographically independent outputs.
- Throws `SodiumError(SODIUM.KDF.INVALID_CONTEXT)` for non-8-byte contexts.
- Throws `SodiumError(SODIUM.KDF.INVALID_LENGTH)` for out-of-range lengths.
- Async — calls `sodiumReady()` internally; no pre-warming required.
## Usage: Basic Example
```ts
import { cryptoKdf } from "@suaveplan/crypto-sodium/primitives/kdf";
const masterKey = crypto.getRandomValues(new Uint8Array(32));
// Derive a 32-byte encryption key (slot 1) and a 32-byte MAC key (slot 2)
const encKey = await cryptoKdf(masterKey, 1, "SuavEncr", 32);
const macKey = await cryptoKdf(masterKey, 2, "SuavEncr", 32);
```
## Usage: Advanced Example
Deriving keys for different subsystems from a single root master:
```ts
import { cryptoKdf } from "@suaveplan/crypto-sodium/primitives/kdf";
const masterKey = crypto.getRandomValues(new Uint8Array(32));
// Context strings must be exactly 8 ASCII bytes:
// "SuavAEAD" for AEAD subkeys, "SuavSign" for signing subkeys
const aeadKey = await cryptoKdf(masterKey, 0, "SuavAEAD", 32);
const signKey = await cryptoKdf(masterKey, 0, "SuavSign", 32);
// Rotate by incrementing subkeyId — old key remains derivable for decryption
const aeadKeyV2 = await cryptoKdf(masterKey, 1, "SuavAEAD", 32);
// Validate context length at compile time:
const CONTEXT = "SuavCtx1"; // exactly 8 chars — enforced at runtime
```
## API Reference
### `cryptoKdf(key, subkeyId, context, length): Promise<Uint8Array>`
Derives a subkey.
**Parameters:**
- `key: Uint8Array` — 32-byte master key.
- `subkeyId: number` — Non-negative integer. Changing this produces a different subkey.
- `context: string` — Must encode to exactly 8 bytes in UTF-8. Acts as a domain separator.
- `length: number` — Output length in bytes (16–64 inclusive).
**Returns:** `Promise<Uint8Array>` of the requested length.
**Throws:**
- `SodiumError(SODIUM.KDF.INVALID_CONTEXT)` — if `context` is not exactly 8 UTF-8 bytes.
- `SodiumError(SODIUM.KDF.INVALID_LENGTH)` — if `length < 16` or `length > 64`.
### `KDF_SUBKEY_MIN = 16`
Minimum accepted output length.
### `KDF_SUBKEY_MAX = 64`
Maximum accepted output length.
## Implementation Notes
The 8-byte context requirement comes directly from libsodium's `crypto_kdf_CONTEXTBYTES` constant. The context is encoded via `TextEncoder` and checked for exact byte length — multi-byte UTF-8 characters can silently cause a 7-char string to produce 8 bytes or a 8-char string to produce more, which is why byte-length is checked rather than character length.
libsodium's `crypto_kdf_derive_from_key` uses BLAKE2b in a mode where the key, subkey ID, context, and output length are mixed into the BLAKE2b input. Outputs for different `subkeyId` values are computationally independent.

View File

@@ -0,0 +1,93 @@
# primitives/crypto-pwhash
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`cryptoPwhashStr` and `cryptoPwhashStrVerify` expose libsodium's Argon2id password hashing via `crypto_pwhash_str` and `crypto_pwhash_str_verify`. Argon2id is the winner of the Password Hashing Competition (2015) and the recommended algorithm for password storage by OWASP and NIST SP 800-63B.
The encoded hash string includes the algorithm identifier, salt, and cost parameters — it is self-contained and safe to store directly in a database. This eliminates a class of bugs where the salt or parameters are stored separately and can become desynchronised.
## Features
- Argon2id password hashing via `crypto_pwhash_str` — salt generated internally.
- Self-contained encoded output (≈97 chars) — includes algorithm, salt, opsLimit, memLimit.
- Constant-time verification via `crypto_pwhash_str_verify`.
- Configurable `opsLimit` and `memLimit` — defaults to `OPSLIMIT_INTERACTIVE` / `MEMLIMIT_INTERACTIVE`.
- Async — calls `sodiumReady()` internally.
- `cryptoPwhashStrVerify` returns `false` on any failure, never throws.
## Usage: Basic Example
```ts
import { cryptoPwhashStr, cryptoPwhashStrVerify } from "@suaveplan/crypto-sodium/primitives/pwhash";
const password = new TextEncoder().encode("user-password-here");
const hash = await cryptoPwhashStr(password);
// Store `hash` in your database. Later, at login:
const valid = await cryptoPwhashStrVerify(hash, password);
console.log(valid); // true
```
## Usage: Advanced Example
Higher-security settings for sensitive credential storage:
```ts
import { cryptoPwhashStr, cryptoPwhashStrVerify } from "@suaveplan/crypto-sodium/primitives/pwhash";
const password = new TextEncoder().encode("admin-master-password");
// Sensitive account — use MODERATE or SENSITIVE preset values
// crypto_pwhash_OPSLIMIT_MODERATE = 3, MEMLIMIT_MODERATE = 268435456 (256 MiB)
const hash = await cryptoPwhashStr(password, {
opsLimit: 3,
memLimit: 268_435_456,
});
// Verification automatically reads cost parameters from the hash string
const ok = await cryptoPwhashStrVerify(hash, password);
// Password rehashing: if ok but hash uses lower-cost params, rehash
// (the encoded string embeds the params so you can detect this)
```
## API Reference
### `cryptoPwhashStr(password, opts?): Promise<string>`
Hashes `password` with Argon2id.
**Parameters:**
- `password: Uint8Array` — Raw password bytes.
- `opts.opsLimit?: number` — CPU/iteration cost. Default: `crypto_pwhash_OPSLIMIT_INTERACTIVE`.
- `opts.memLimit?: number` — Memory cost in bytes. Default: `crypto_pwhash_MEMLIMIT_INTERACTIVE`.
**Returns:** `Promise<string>` — Argon2id-encoded hash string, safe for database storage.
### `cryptoPwhashStrVerify(hash, password): Promise<boolean>`
Verifies `password` against an encoded hash string.
**Parameters:**
- `hash: string` — Encoded hash from `cryptoPwhashStr`.
- `password: Uint8Array` — Candidate password bytes.
**Returns:** `Promise<boolean>` — `true` if the password matches. Returns `false` (not throws) on any failure including malformed hash strings.
### `interface PwhashOptions`
- `opsLimit?: number`
- `memLimit?: number`
## Implementation Notes
`crypto_pwhash_str` generates a random salt internally and encodes the full Argon2id parameter set into the output string. This is the `$argon2id$v=19$...` format, compatible with most password-hashing libraries. The salt is never exposed to the caller.
`crypto_pwhash_str_verify` uses constant-time comparison for the hash check. The `try/catch` maps libsodium exceptions (e.g., malformed hash strings, wrong length) to `false` for uniform API behaviour.
The interactive preset (`OPSLIMIT_INTERACTIVE`, `MEMLIMIT_INTERACTIVE`) is calibrated for ≈50 ms on typical hardware. Increase `opsLimit` and `memLimit` for stored API keys, encryption master passwords, or recovery codes.

View File

@@ -0,0 +1,98 @@
# primitives/ristretto255
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
The `ristretto255` namespace exposes libsodium's Ristretto255 group operations — a prime-order group construction over Curve25519 that eliminates the cofactor pitfalls present in raw Edwards25519 arithmetic. Ristretto255 is used in zero-knowledge proofs, Oblivious Pseudorandom Functions (OPRFs), Password-Authenticated Key Exchanges (PAKEs), and Verifiable Random Functions (VRFs).
By wrapping libsodium's `crypto_core_ristretto255_*` and `crypto_scalarmult_ristretto255_*` APIs, this module provides the building blocks for advanced cryptographic protocols without requiring callers to reason about low-level curve arithmetic or cofactor clearing.
## Features
- Group element addition (`add`) and subtraction (`sub`) via `crypto_core_ristretto255_add/sub`.
- Scalar multiplication (`mul`) via `crypto_scalarmult_ristretto255`.
- Hash-to-curve via `fromHash` using `crypto_core_ristretto255_from_hash` (64-byte input).
- Point serialisation (`toBytes`) and deserialisation with validation (`fromBytes`).
- Random group element generation (`random`) via `crypto_core_ristretto255_random`.
- All operations are async and call `sodiumReady()` internally.
- `fromBytes` throws `RangeError` for invalid or identity points.
## Usage: Basic Example
```ts
import { ristretto255 } from "@suaveplan/crypto-sodium/primitives/ristretto255";
// Generate two random group elements
const p = await ristretto255.random();
const q = await ristretto255.random();
// Add them
const sum = await ristretto255.add(p, q);
// Verify commutativity
const sumQP = await ristretto255.add(q, p);
// sum deepEquals sumQP
```
## Usage: Advanced Example
Hash-to-curve for OPRF input blinding:
```ts
import { ristretto255 } from "@suaveplan/crypto-sodium/primitives/ristretto255";
// Hash the password to a group element (hash-to-curve)
// In a real OPRF you'd use a proper H2C function; fromHash works for demos
async function hashToPoint(input: Uint8Array): Promise<Uint8Array> {
// Pad/hash input to 64 bytes with SHA-512 first in production:
const padded = new Uint8Array(64);
padded.set(input.subarray(0, Math.min(input.length, 64)));
return ristretto255.fromHash(padded);
}
// Scalar multiplication: client blinds the point with a random scalar
const blindScalar = new Uint8Array(32);
crypto.getRandomValues(blindScalar);
const point = await hashToPoint(new TextEncoder().encode("password"));
const blinded = await ristretto255.mul(blindScalar, point);
```
## API Reference
### `ristretto255.add(p, q): Promise<Uint8Array>`
Adds two group elements. Commutative: `add(p, q) === add(q, p)`.
### `ristretto255.sub(p, q): Promise<Uint8Array>`
Subtracts `q` from `p`. Inverse of `add`.
### `ristretto255.mul(scalar, point): Promise<Uint8Array>`
Scalar multiplication: computes `scalar * point`. Scalar is a 32-byte little-endian integer.
### `ristretto255.fromHash(hash): Promise<Uint8Array>`
Maps a 64-byte hash uniformly to a group element via `crypto_core_ristretto255_from_hash`.
### `ristretto255.toBytes(point): Promise<Uint8Array>`
Returns a copy of the 32-byte canonical encoding of `point`.
### `ristretto255.fromBytes(bytes): Promise<Uint8Array>`
Validates and returns a group element. **Throws `RangeError`** if `bytes` is not a valid, non-identity Ristretto255 point.
### `ristretto255.random(): Promise<Uint8Array>`
Generates a uniformly random 32-byte group element.
## Implementation Notes
Ristretto255 represents each group element as a 32-byte canonical encoding. Unlike raw Edwards25519, every 32-byte encoding maps to at most one group element — there are no encoding ambiguities or cofactor issues.
`fromBytes` calls `crypto_core_ristretto255_is_valid_point` before returning, which rejects the identity element (all-zeros) and any bytes that do not correspond to a valid Ristretto255 point. This prevents callers from accidentally working with invalid inputs in downstream protocols.
All operations return new `Uint8Array` allocations; the input arrays are not mutated.