Move docs/ package mirror into document-navigator plugin
Relocate the ~6k-file docs tree from the repo root into plugins/document-navigator/docs/ so the plugin is self-contained and portable. docs/superpowers/ (specs and plans) stays at the repo root. Moved via cp+rm rather than git mv because directory rename() fails with EPERM on this WSL /mnt DrvFs mount; git rename-detection pairs the identical-content files at commit time. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,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
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
Reference in New Issue
Block a user