first commit
This commit is contained in:
125
docs/security-stack/crypto/crypto-core/src/aead/aead.md
Normal file
125
docs/security-stack/crypto/crypto-core/src/aead/aead.md
Normal file
@@ -0,0 +1,125 @@
|
||||
# aead
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
- [noble-aead-engine](./noble-aead-engine/noble-aead-engine.md) — <!-- begin:auto-docs --> _(no submodules)_- [aead](./aead.md) — <!-- begin:auto-docs --> _(no submodules)_- noble-aead-engine — <!-- begin:auto-docs --> _(no su…
|
||||
- [noble-aead-engine](./noble-aead-engine/noble-aead-engine.md) — <!-- begin:auto-docs --> _(no submodules)_- [aead](./aead.md) — <!-- begin:auto-docs --> _(no submodules)_- [noble-aead-engine](./noble-aead-engine/noble-aead-engine.md) — <!-- begin:auto-docs --> _(no submodules)_- [aead](./aead.md) — <!-- begin:auto-docs --> _(no submodules)_- noble-aead-engine — <!-- beg…
|
||||
- [noble-aead-engine](./noble-aead-engine/noble-aead-engine.md) — <!-- begin:auto-docs --> _(no submodules)_- [aead](./aead.md) — <!-- begin:auto-docs --> _(no submodules)_- noble-aead-engine — <!-- begin:auto-docs --> _(no s…
|
||||
- [noble-aead-engine](./noble-aead-engine/noble-aead-engine.md) — <!-- begin:auto-docs --> _(no submodules)_- [aead](./aead.md) — <!-- begin:auto-docs --> _(no submodules)_
|
||||
## Purpose
|
||||
|
||||
Provides authenticated encryption with associated data (AEAD) facade functions backed by `@noble/ciphers`. AEAD combines confidentiality and integrity in a single operation — encrypted data includes an authentication tag that is verified before decryption, making it safe against ciphertext tampering. The facade is engine-pluggable: pass `{ engine }` to swap in a WebCrypto-backed or libsodium-backed implementation without changing calling code.
|
||||
|
||||
## Features
|
||||
|
||||
- Three algorithms: `AES-256-GCM`, `ChaCha20-Poly1305`, and `XChaCha20-Poly1305`
|
||||
- Engine-pluggable via `AeadEngine` interface (noble default, WebCrypto, libsodium)
|
||||
- Key validation: rejects non-32-byte keys with `CRYPTO.KEY.INVALID_LENGTH`
|
||||
- Nonce validation: 12 bytes for AES-GCM and ChaCha20, 24 bytes for XChaCha20
|
||||
- Decrypt never leaks plaintext in error context
|
||||
- Optional AAD (Additional Authenticated Data) for binding ciphertext to context
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { aeadEncrypt, aeadDecrypt, randomBytes } from "@suaveplan/crypto-core";
|
||||
|
||||
const key = randomBytes(32);
|
||||
const nonce = randomBytes(12);
|
||||
const plaintext = new TextEncoder().encode("hello world");
|
||||
|
||||
const ciphertext = aeadEncrypt("AES-256-GCM", key, plaintext, { nonce });
|
||||
const recovered = aeadDecrypt("AES-256-GCM", key, ciphertext, { nonce });
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { aeadEncrypt, aeadDecrypt, randomBytes } from "@suaveplan/crypto-core";
|
||||
import type { AeadEngine } from "@suaveplan/crypto-core/engines";
|
||||
|
||||
// Custom engine stub for testing
|
||||
const stubEngine: AeadEngine = {
|
||||
encrypt: (_key, pt) => pt, // identity pass-through (test only!)
|
||||
decrypt: (_key, ct) => ct,
|
||||
};
|
||||
|
||||
// XChaCha20-Poly1305 with 24-byte nonce and AAD
|
||||
const key = randomBytes(32);
|
||||
const nonce = randomBytes(24);
|
||||
const aad = new TextEncoder().encode("request-id:abc123");
|
||||
const plaintext = new Uint8Array([1, 2, 3]);
|
||||
|
||||
const ct = aeadEncrypt("XChaCha20-Poly1305", key, plaintext, { nonce, aad });
|
||||
const pt = aeadDecrypt("XChaCha20-Poly1305", key, ct, { nonce, aad });
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `AeadAlgorithm`
|
||||
|
||||
```ts
|
||||
type AeadAlgorithm = "AES-256-GCM" | "ChaCha20-Poly1305" | "XChaCha20-Poly1305"
|
||||
```
|
||||
|
||||
### `AEAD_KEY_LENGTH`
|
||||
|
||||
```ts
|
||||
const AEAD_KEY_LENGTH = 32
|
||||
```
|
||||
|
||||
Required key length in bytes for all algorithms.
|
||||
|
||||
### `AEAD_NONCE_LENGTH`
|
||||
|
||||
```ts
|
||||
const AEAD_NONCE_LENGTH = 12
|
||||
```
|
||||
|
||||
Required nonce length for AES-256-GCM and ChaCha20-Poly1305.
|
||||
|
||||
### `AEAD_XCHACHA_NONCE_LENGTH`
|
||||
|
||||
```ts
|
||||
const AEAD_XCHACHA_NONCE_LENGTH = 24
|
||||
```
|
||||
|
||||
Required nonce length for XChaCha20-Poly1305.
|
||||
|
||||
### `aeadEncrypt(algo, key, plaintext, options, opts?)`
|
||||
|
||||
```ts
|
||||
function aeadEncrypt(
|
||||
algo: AeadAlgorithm,
|
||||
key: Uint8Array,
|
||||
plaintext: Uint8Array,
|
||||
options: AeadEncryptOptions,
|
||||
opts?: AeadEngineOptions
|
||||
): Uint8Array
|
||||
```
|
||||
|
||||
Encrypts and authenticates `plaintext`. Returns `ciphertext || tag` (16-byte tag appended).
|
||||
|
||||
**Throws:** `CryptoError("CRYPTO.KEY.INVALID_LENGTH")` — key is not 32 bytes.
|
||||
**Throws:** `CryptoError("CRYPTO.AEAD.INVALID_NONCE")` — nonce has wrong length for the algorithm.
|
||||
|
||||
### `aeadDecrypt(algo, key, ciphertext, options, opts?)`
|
||||
|
||||
```ts
|
||||
function aeadDecrypt(
|
||||
algo: AeadAlgorithm,
|
||||
key: Uint8Array,
|
||||
ciphertext: Uint8Array,
|
||||
options: AeadDecryptOptions,
|
||||
opts?: AeadEngineOptions
|
||||
): Uint8Array
|
||||
```
|
||||
|
||||
Verifies authentication tag and decrypts.
|
||||
|
||||
**Throws:** `CryptoError("CRYPTO.AEAD.DECRYPT_FAILED")` — tag mismatch or corrupted ciphertext. Error context never includes key or plaintext bytes.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The noble library AES-256-GCM uses the native Web Crypto API when available for hardware acceleration. ChaCha20-Poly1305 and XChaCha20-Poly1305 are implemented in pure JavaScript, making them the better choice on platforms without AES hardware support (e.g., older ARM). XChaCha20-Poly1305 accepts a 24-byte nonce, which is large enough to generate randomly without birthday-bound concerns for up to 2^96 messages with the same key — whereas the 12-byte nonce of standard ChaCha20-Poly1305 requires a counter or careful uniqueness management.
|
||||
@@ -0,0 +1,103 @@
|
||||
# NobleAeadEngine
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
`NobleAeadEngine` is the default AEAD (Authenticated Encryption with Associated Data) engine for `@suaveplan/crypto`. It delegates to `@suaveplan/crypto-cipher` — itself backed by `@noble/ciphers` — so the build-time primitive selector defined in `crypto.config.ts` applies uniformly to all cipher calls, whether from the high-level facade or from this engine directly. The engine supports three algorithms: AES-256-GCM, ChaCha20-Poly1305, and XChaCha20-Poly1305, selected per instance at construction time.
|
||||
|
||||
## Features
|
||||
|
||||
- Implements the `AeadEngine` interface, making it pluggable into `aeadEncrypt` / `aeadDecrypt`
|
||||
- Supports `AES-256-GCM`, `ChaCha20-Poly1305`, and `XChaCha20-Poly1305` via `AeadAlgorithm` discriminant
|
||||
- Decryption failures are caught and re-thrown as `CryptoError` with code `AEAD.DECRYPT_FAILED` and structured context (algorithm name, ciphertext byte length)
|
||||
- Algorithm selection delegates through `@suaveplan/crypto-cipher` subpaths, keeping the build-time selector effective
|
||||
- Nonce is caller-supplied; the engine never generates nonces internally
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```typescript
|
||||
import { NobleAeadEngine } from "@suaveplan/crypto-core/aead/noble-aead-engine";
|
||||
import { randomBytes } from "@suaveplan/crypto-core";
|
||||
|
||||
const engine = new NobleAeadEngine("XChaCha20-Poly1305");
|
||||
const key = randomBytes(32);
|
||||
const nonce = randomBytes(24); // 24 bytes for XChaCha20
|
||||
const plaintext = new TextEncoder().encode("hello world");
|
||||
|
||||
const ciphertext = engine.encrypt(key, plaintext, nonce);
|
||||
const recovered = engine.decrypt(key, ciphertext, nonce);
|
||||
|
||||
console.log(new TextDecoder().decode(recovered)); // "hello world"
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```typescript
|
||||
import { NobleAeadEngine } from "@suaveplan/crypto-core/aead/noble-aead-engine";
|
||||
import { CryptoError } from "@suaveplan/crypto-core/errors/crypto-error";
|
||||
import { CRYPTO_CODES } from "@suaveplan/crypto-core/errors/codes";
|
||||
import { randomBytes } from "@suaveplan/crypto-core";
|
||||
|
||||
const engine = new NobleAeadEngine("AES-256-GCM");
|
||||
const key = randomBytes(32);
|
||||
const nonce = randomBytes(12); // 12 bytes for AES-256-GCM
|
||||
|
||||
// Encrypt with additional authenticated data (AAD)
|
||||
const plaintext = new TextEncoder().encode("sensitive payload");
|
||||
const aad = new TextEncoder().encode("request-id:abc123");
|
||||
|
||||
const ciphertext = engine.encrypt(key, plaintext, nonce, aad);
|
||||
|
||||
// Verify AAD is checked during decrypt
|
||||
try {
|
||||
const wrongAad = new TextEncoder().encode("request-id:wrong");
|
||||
engine.decrypt(key, ciphertext, nonce, wrongAad);
|
||||
} catch (err) {
|
||||
if (err instanceof CryptoError && err.code === CRYPTO_CODES["AEAD.DECRYPT_FAILED"]) {
|
||||
console.log("AAD mismatch detected — message rejected");
|
||||
}
|
||||
}
|
||||
|
||||
// Correct AAD succeeds
|
||||
const recovered = engine.decrypt(key, ciphertext, nonce, aad);
|
||||
console.log(new TextDecoder().decode(recovered)); // "sensitive payload"
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `NobleAeadEngine`
|
||||
|
||||
```typescript
|
||||
class NobleAeadEngine implements AeadEngine
|
||||
```
|
||||
|
||||
**Constructor**
|
||||
|
||||
| Parameter | Type | Description |
|
||||
|-----------|-----------------|-------------------------------------------------|
|
||||
| `algo` | `AeadAlgorithm` | Selects the cipher: `"AES-256-GCM"`, `"ChaCha20-Poly1305"`, or `"XChaCha20-Poly1305"`. |
|
||||
|
||||
**`encrypt(key, plaintext, nonce, aad?): Uint8Array`**
|
||||
|
||||
| Parameter | Type | Description |
|
||||
|-------------|----------------|------------------------------------------------------|
|
||||
| `key` | `Uint8Array` | Symmetric key (32 bytes for all supported algorithms). |
|
||||
| `plaintext` | `Uint8Array` | Message bytes to encrypt. |
|
||||
| `nonce` | `Uint8Array` | Unique nonce (12 bytes for AES-GCM; 12 or 24 bytes for ChaCha variants). |
|
||||
| `aad` | `Uint8Array \| undefined` | Optional additional authenticated data. |
|
||||
|
||||
Returns the ciphertext with authentication tag appended (16-byte tag for all three algorithms).
|
||||
|
||||
**`decrypt(key, ciphertext, nonce, aad?): Uint8Array`**
|
||||
|
||||
Same parameters as `encrypt`, with `ciphertext` in place of `plaintext`.
|
||||
|
||||
Throws `CryptoError` with code `CRYPTO_CODES["AEAD.DECRYPT_FAILED"]` if authentication verification fails or if the underlying primitive rejects the ciphertext for any reason. The error's `context` field includes `algorithm` and `ciphertextLength`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The internal `primitiveFor` function maps the `AeadAlgorithm` discriminant to the matching `@suaveplan/crypto-cipher` subpath import (`aes-256-gcm`, `xchacha20-poly1305`, or `chacha20-poly1305`). This avoids a dynamic dispatch switch on every call while still keeping each cipher behind its own subpath so tree-shaking is effective.
|
||||
|
||||
Encryption is allowed to propagate raw errors from the primitive (malformed key length, nonce length mismatch) because these represent programming errors — the caller supplied invalid arguments — rather than runtime failures from untrusted input. Decryption is wrapped in a try/catch because failures there arise from untrusted ciphertext and must be surfaced as a stable, typed `CryptoError` rather than a raw `Error` with an implementation-detail message from `@noble/ciphers`.
|
||||
@@ -0,0 +1,89 @@
|
||||
# NobleX25519Engine
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
`NobleX25519Engine` is the default key-agreement engine for `@suaveplan/crypto`. It implements the `AgreeEngine` interface using X25519 Diffie-Hellman as exposed through `@suaveplan/crypto-curve`, which in turn delegates to `@noble/curves`. The engine handles both ephemeral keypair generation and shared-secret derivation. It is consumed by the `x25519Agree` facade function and can be swapped for alternative implementations (e.g. the WebCrypto or libsodium engines from sibling packages) via the engine option on `@suaveplan/crypto`'s public API.
|
||||
|
||||
## Features
|
||||
|
||||
- Implements `AgreeEngine` for X25519 Diffie-Hellman key agreement
|
||||
- Keypair generation accepts an optional `ByteSource` for deterministic testing without mocks
|
||||
- X25519 internal scalar clamping means every 32-byte value is a valid secret key — no rejection-sampling loop required
|
||||
- Delegates through `@suaveplan/crypto-curve/x25519` so the build-time primitive selector applies
|
||||
- Returns the raw 32-byte shared secret; key derivation (HKDF, etc.) is the caller's responsibility
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```typescript
|
||||
import { NobleX25519Engine } from "@suaveplan/crypto-core/agree/noble-agree-engine";
|
||||
|
||||
const engine = new NobleX25519Engine();
|
||||
|
||||
const alice = engine.generateKeypair();
|
||||
const bob = engine.generateKeypair();
|
||||
|
||||
// Both sides compute the same shared secret
|
||||
const aliceShared = engine.agree(alice.secretKey, bob.publicKey);
|
||||
const bobShared = engine.agree(bob.secretKey, alice.publicKey);
|
||||
|
||||
console.log(aliceShared.length); // 32
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```typescript
|
||||
import { NobleX25519Engine } from "@suaveplan/crypto-core/agree/noble-agree-engine";
|
||||
import { hkdf } from "@suaveplan/crypto-hash";
|
||||
|
||||
const engine = new NobleX25519Engine();
|
||||
|
||||
// Use a deterministic byte source for reproducible tests
|
||||
const fixedBytes = (n: number) => new Uint8Array(n).fill(0xab);
|
||||
const aliceKeypair = engine.generateKeypair(fixedBytes);
|
||||
|
||||
const bob = engine.generateKeypair();
|
||||
|
||||
// Derive a session key from the raw shared secret
|
||||
const rawSecret = engine.agree(aliceKeypair.secretKey, bob.publicKey);
|
||||
const sessionKey = hkdf("SHA-256", rawSecret, {
|
||||
salt: new Uint8Array(32),
|
||||
info: new TextEncoder().encode("session-key-v1"),
|
||||
length: 32,
|
||||
});
|
||||
|
||||
console.log(sessionKey.length); // 32
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `NobleX25519Engine`
|
||||
|
||||
```typescript
|
||||
class NobleX25519Engine implements AgreeEngine
|
||||
```
|
||||
|
||||
**`generateKeypair(byteSource?): AgreeKeypair`**
|
||||
|
||||
| Parameter | Type | Description |
|
||||
|--------------|-------------------------|--------------------------------------------------------------|
|
||||
| `byteSource` | `ByteSource \| undefined` | Optional source of random bytes. Defaults to `defaultByteSource` (secure random). |
|
||||
|
||||
Returns `AgreeKeypair { publicKey: Uint8Array; secretKey: Uint8Array }` where both keys are 32 bytes.
|
||||
|
||||
**`agree(mySecret, theirPublic): Uint8Array`**
|
||||
|
||||
| Parameter | Type | Description |
|
||||
|---------------|--------------|-------------------------------------------|
|
||||
| `mySecret` | `Uint8Array` | Local 32-byte X25519 secret key. |
|
||||
| `theirPublic` | `Uint8Array` | Remote 32-byte X25519 public key. |
|
||||
|
||||
Returns a 32-byte raw shared secret. Throws if either key is malformed (propagated from the underlying curve implementation).
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
X25519 applies a clamping operation to the scalar before performing the Diffie-Hellman multiplication. This means any 32-byte sequence is a valid secret key — the clamp adjusts bits 0, 1, 2, 7, and 255 to ensure the scalar lands in the correct subgroup. As a result, `generateKeypair` simply draws 32 bytes from the byte source and passes them directly to `getPublicKey`; no validity check or retry loop is needed.
|
||||
|
||||
The raw shared secret returned by `agree` has the structure of a group element, not a uniformly random key. Callers performing symmetric encryption should always pass the output through a key derivation function (HKDF is recommended) before using it as a cipher key. The higher-level `authBox` and `sealedBox` constructs in this package perform HKDF-SHA256 derivation automatically.
|
||||
@@ -0,0 +1,83 @@
|
||||
# x25519
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
Provides X25519 Elliptic-Curve Diffie-Hellman (ECDH) key agreement — key generation and shared-secret derivation — backed by `@noble/curves`. X25519 is a state-of-the-art key-agreement protocol over Curve25519 offering 128-bit security, fast constant-time scalar multiplication, and immunity to many implementation pitfalls of older curve designs. The resulting shared secret should be passed through a KDF (e.g., HKDF via `@suaveplan/crypto-hash`) before use as a symmetric key. The facade is engine-pluggable: pass `{ engine }` to substitute a WebCrypto ECDH or libsodium-backed implementation.
|
||||
|
||||
## Features
|
||||
|
||||
- Key generation using injected `ByteSource` (testable) or `crypto.getRandomValues` (production)
|
||||
- X25519 scalar clamping applied internally — every 32-byte value is a valid secret key
|
||||
- Returns raw 32-byte shared secret (pass through HKDF before symmetric use)
|
||||
- Engine-pluggable via `AgreeEngine` interface
|
||||
- `ByteSource` / deprecated `rng` option for key-generation injection
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { x25519 } from "@suaveplan/crypto-core";
|
||||
|
||||
const alice = x25519.generateKeypair();
|
||||
const bob = x25519.generateKeypair();
|
||||
|
||||
const shared1 = x25519.agree(alice.secretKey, bob.publicKey);
|
||||
const shared2 = x25519.agree(bob.secretKey, alice.publicKey);
|
||||
// shared1 deep-equals shared2
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { x25519 } from "@suaveplan/crypto-core";
|
||||
import { hkdf } from "@suaveplan/crypto-hash";
|
||||
|
||||
// Key agreement + HKDF key derivation
|
||||
const alice = x25519.generateKeypair();
|
||||
const bob = x25519.generateKeypair();
|
||||
|
||||
const rawSecret = x25519.agree(alice.secretKey, bob.publicKey);
|
||||
const encKey = hkdf("SHA-256", rawSecret, {
|
||||
salt: new Uint8Array(32),
|
||||
info: new TextEncoder().encode("enc-key"),
|
||||
length: 32,
|
||||
});
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `X25519Keypair`
|
||||
|
||||
```ts
|
||||
type X25519Keypair = { publicKey: Uint8Array; secretKey: Uint8Array }
|
||||
```
|
||||
|
||||
Both keys are 32 bytes.
|
||||
|
||||
### `generateKeypair(opts?)`
|
||||
|
||||
```ts
|
||||
function generateKeypair(opts?: X25519Options): X25519Keypair
|
||||
```
|
||||
|
||||
Generates a fresh X25519 keypair. X25519 clamps the scalar internally, so any 32 random bytes form a valid key.
|
||||
|
||||
| Param | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `opts.byteSource` | `ByteSource?` | Byte source for key material. |
|
||||
| `opts.rng` | `ByteSource?` | Deprecated alias for `byteSource`. |
|
||||
| `opts.engine` | `AgreeEngine?` | Override the default noble engine. |
|
||||
|
||||
### `agree(mySecret, theirPublic, opts?)`
|
||||
|
||||
```ts
|
||||
function agree(mySecret: Uint8Array, theirPublic: Uint8Array, opts?: { engine?: AgreeEngine }): Uint8Array
|
||||
```
|
||||
|
||||
Performs X25519 ECDH and returns the 32-byte shared secret. Both parties calling `agree` with their own secret key and the other party's public key will derive the same secret.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The raw X25519 shared secret must be passed through a KDF such as HKDF before use as a symmetric encryption key. Raw DH outputs have high entropy but are not uniformly distributed and may be biased in edge cases. See `@suaveplan/crypto-hash` for `hkdf`. The `agree` function is called `getSharedSecret` in the noble library — the facade renames it for clarity in the key-agreement context.
|
||||
103
docs/security-stack/crypto/crypto-core/src/auth-box/auth-box.md
Normal file
103
docs/security-stack/crypto/crypto-core/src/auth-box/auth-box.md
Normal file
@@ -0,0 +1,103 @@
|
||||
# auth-box
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
- [noble-auth-box-engine](./noble-auth-box-engine/noble-auth-box-engine.md) — <!-- begin:auto-docs --> _(no submodules)_- [auth-box](./auth-box.md) — <!-- begin:auto-docs --> _(no submodules)_- noble-auth-box-engine — <!-- begin:auto-docs --> _(n…
|
||||
- [noble-auth-box-engine](./noble-auth-box-engine/noble-auth-box-engine.md) — <!-- begin:auto-docs --> _(no submodules)_- [auth-box](./auth-box.md) — <!-- begin:auto-docs --> _(no submodules)_- [noble-auth-box-engine](./noble-auth-box-engine/noble-auth-box-engine.md) — <!-- begin:auto-docs --> _(no submodules)_- [auth-box](./auth-box.md) — <!-- begin:auto-docs --> _(no submodules)_- noble-auth-box-engine — <!--…
|
||||
- [noble-auth-box-engine](./noble-auth-box-engine/noble-auth-box-engine.md) — <!-- begin:auto-docs --> _(no submodules)_- [auth-box](./auth-box.md) — <!-- begin:auto-docs --> _(no submodules)_- noble-auth-box-engine — <!-- begin:auto-docs --> _(…
|
||||
- [noble-auth-box-engine](./noble-auth-box-engine/noble-auth-box-engine.md) — <!-- begin:auto-docs --> _(no submodules)_- [auth-box](./auth-box.md) — <!-- begin:auto-docs --> _(no submodules)_
|
||||
## Purpose
|
||||
|
||||
Provides authenticated sealed-box encryption following `crypto_box` semantics. Unlike the anonymous `sealedBox`, the auth-box binds the sender's identity to the ciphertext: the recipient can verify that only the holder of the sender's secret key could have produced it. The protocol uses X25519 ECDH key agreement, HKDF-SHA256 key derivation, and XChaCha20-Poly1305 encryption. Both parties must hold each other's public keys before communicating, making this suitable for protocols with established key exchange such as Signal-style messaging or server-to-server encryption.
|
||||
|
||||
## Features
|
||||
|
||||
- Authenticated sender — recipient verifies sender identity implicitly via ECDH
|
||||
- X25519 ECDH + HKDF-SHA256 + XChaCha20-Poly1305 (crypto_box semantics)
|
||||
- Caller-supplied 24-byte nonce for fine-grained nonce management
|
||||
- Engine-pluggable via `AuthBoxEngine` interface
|
||||
- Authenticated decryption — rejects tampered ciphertext with `CRYPTO.AEAD.DECRYPT_FAILED`
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { authBox, x25519 } from "@suaveplan/crypto-core";
|
||||
import { randomBytes } from "@suaveplan/crypto-core";
|
||||
|
||||
const alice = x25519.generateKeypair();
|
||||
const bob = x25519.generateKeypair();
|
||||
const nonce = randomBytes(24);
|
||||
const msg = new TextEncoder().encode("authenticated message");
|
||||
|
||||
// Alice seals for Bob
|
||||
const ct = authBox.seal(alice.secretKey, bob.publicKey, msg, nonce);
|
||||
|
||||
// Bob opens from Alice
|
||||
const pt = authBox.open(alice.publicKey, bob.secretKey, ct, nonce);
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { authBox, x25519, randomBytes } from "@suaveplan/crypto-core";
|
||||
import type { AuthBoxEngine } from "@suaveplan/crypto-core/engines";
|
||||
|
||||
// Custom engine stub for testing
|
||||
const stubEngine: AuthBoxEngine = {
|
||||
seal: (_senderSec, _recipPub, pt, _nonce) => pt.slice(),
|
||||
open: (_senderPub, _recipSec, ct, _nonce) => ct.slice(),
|
||||
};
|
||||
|
||||
const alice = x25519.generateKeypair();
|
||||
const bob = x25519.generateKeypair();
|
||||
const nonce = randomBytes(24);
|
||||
const msg = new Uint8Array([0x42]);
|
||||
|
||||
const ct = authBox.seal(alice.secretKey, bob.publicKey, msg, nonce, { engine: stubEngine });
|
||||
const pt = authBox.open(alice.publicKey, bob.secretKey, ct, nonce, { engine: stubEngine });
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `seal(senderSecret, recipientPublic, plaintext, nonce, opts?)`
|
||||
|
||||
```ts
|
||||
function seal(
|
||||
senderSecret: Uint8Array,
|
||||
recipientPublic: Uint8Array,
|
||||
plaintext: Uint8Array,
|
||||
nonce: Uint8Array,
|
||||
opts?: AuthBoxEngineOptions
|
||||
): Uint8Array
|
||||
```
|
||||
|
||||
Encrypts and authenticates `plaintext`. Returns the XChaCha20-Poly1305 ciphertext + 16-byte tag.
|
||||
|
||||
| Param | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `senderSecret` | `Uint8Array` | Sender's X25519 secret key (32 bytes). |
|
||||
| `recipientPublic` | `Uint8Array` | Recipient's X25519 public key (32 bytes). |
|
||||
| `plaintext` | `Uint8Array` | Data to encrypt. |
|
||||
| `nonce` | `Uint8Array` | 24-byte nonce. MUST be unique per (senderKey, recipientKey) pair. |
|
||||
| `opts.engine` | `AuthBoxEngine?` | Override the default noble engine. |
|
||||
|
||||
### `open(senderPublic, recipientSecret, ciphertext, nonce, opts?)`
|
||||
|
||||
```ts
|
||||
function open(
|
||||
senderPublic: Uint8Array,
|
||||
recipientSecret: Uint8Array,
|
||||
ciphertext: Uint8Array,
|
||||
nonce: Uint8Array,
|
||||
opts?: AuthBoxEngineOptions
|
||||
): Uint8Array
|
||||
```
|
||||
|
||||
Verifies and decrypts. Throws on authentication failure.
|
||||
|
||||
**Throws:** `CryptoError("CRYPTO.AEAD.DECRYPT_FAILED")` — tag mismatch or corrupted ciphertext.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The HKDF info string `"suaveplan-auth-box-v1"` is domain-separated from the sealed-box protocol. Authentication works because the shared secret is derived from `ECDH(senderSecret, recipientPublic)` which equals `ECDH(recipientSecret, senderPublic)` — only the holders of the respective secret keys can compute this value. The caller is responsible for nonce uniqueness; a counter concatenated with a random prefix, or a large random nonce (24 bytes, safe for random generation), are both acceptable strategies.
|
||||
@@ -0,0 +1,111 @@
|
||||
# NobleAuthBoxEngine
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
`NobleAuthBoxEngine` implements the `AuthBoxEngine` interface using a `crypto_box`-style authenticated encryption protocol backed by `@noble/curves` and `@noble/ciphers` (via the `@suaveplan/crypto-curve` and `@suaveplan/crypto-cipher` subpaths). Unlike a sealed-box, an auth-box retains sender identity: the sender's long-term secret key is used in the ECDH step, allowing the recipient to verify that the message came from the expected party. The nonce is caller-supplied; callers are responsible for uniqueness per sender-recipient key pair.
|
||||
|
||||
## Features
|
||||
|
||||
- Implements `AuthBoxEngine` (`seal` / `open`) for two-party authenticated encryption
|
||||
- Protocol: X25519 ECDH(senderSecret, recipientPublic) → HKDF-SHA256 → XChaCha20-Poly1305
|
||||
- HKDF uses a fixed zero salt and a domain-separation info string `"suaveplan-auth-box-v1"`
|
||||
- Decryption failures surface as `CryptoError` with code `AEAD.DECRYPT_FAILED`
|
||||
- Caller supplies the nonce; the engine does not generate nonces internally
|
||||
- Delegates to `@suaveplan/crypto-cipher/xchacha20-poly1305` and `@suaveplan/crypto-curve/x25519` for build-time primitive selection
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```typescript
|
||||
import { NobleAuthBoxEngine } from "@suaveplan/crypto-core/auth-box/noble-auth-box-engine";
|
||||
import { randomBytes } from "@suaveplan/crypto-core";
|
||||
|
||||
const engine = new NobleAuthBoxEngine();
|
||||
|
||||
// Generate sender and recipient keypairs (X25519)
|
||||
const sender = { secretKey: randomBytes(32), publicKey: /* derive */ new Uint8Array(32) };
|
||||
const recipient = { secretKey: randomBytes(32), publicKey: /* derive */ new Uint8Array(32) };
|
||||
|
||||
const nonce = randomBytes(24);
|
||||
const plaintext = new TextEncoder().encode("hello from sender");
|
||||
|
||||
const ciphertext = engine.seal(sender.secretKey, recipient.publicKey, plaintext, nonce);
|
||||
const recovered = engine.open(sender.publicKey, recipient.secretKey, ciphertext, nonce);
|
||||
|
||||
console.log(new TextDecoder().decode(recovered)); // "hello from sender"
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```typescript
|
||||
import { NobleAuthBoxEngine } from "@suaveplan/crypto-core/auth-box/noble-auth-box-engine";
|
||||
import { NobleX25519Engine } from "@suaveplan/crypto-core/agree/noble-agree-engine";
|
||||
import { CryptoError } from "@suaveplan/crypto-core/errors/crypto-error";
|
||||
import { CRYPTO_CODES } from "@suaveplan/crypto-core/errors/codes";
|
||||
import { randomBytes } from "@suaveplan/crypto-core";
|
||||
|
||||
const agreeEngine = new NobleX25519Engine();
|
||||
const authBoxEngine = new NobleAuthBoxEngine();
|
||||
|
||||
const alice = agreeEngine.generateKeypair();
|
||||
const bob = agreeEngine.generateKeypair();
|
||||
|
||||
const nonce = randomBytes(24);
|
||||
const message = new TextEncoder().encode("authenticated message from alice");
|
||||
|
||||
// Alice seals a message to Bob
|
||||
const ciphertext = authBoxEngine.seal(alice.secretKey, bob.publicKey, message, nonce);
|
||||
|
||||
// Bob opens it using Alice's public key for verification
|
||||
const plaintext = authBoxEngine.open(alice.publicKey, bob.secretKey, ciphertext, nonce);
|
||||
console.log(new TextDecoder().decode(plaintext));
|
||||
|
||||
// Tampered ciphertext is rejected
|
||||
try {
|
||||
const tampered = ciphertext.slice();
|
||||
tampered[tampered.length - 1] ^= 0xff;
|
||||
authBoxEngine.open(alice.publicKey, bob.secretKey, tampered, nonce);
|
||||
} catch (err) {
|
||||
if (err instanceof CryptoError && err.code === CRYPTO_CODES["AEAD.DECRYPT_FAILED"]) {
|
||||
console.log("Tampered ciphertext correctly rejected");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `NobleAuthBoxEngine`
|
||||
|
||||
```typescript
|
||||
class NobleAuthBoxEngine implements AuthBoxEngine
|
||||
```
|
||||
|
||||
**`seal(senderSecret, recipientPublic, plaintext, nonce): Uint8Array`**
|
||||
|
||||
| Parameter | Type | Description |
|
||||
|------------------|--------------|----------------------------------------------------------|
|
||||
| `senderSecret` | `Uint8Array` | Sender's 32-byte X25519 secret key. |
|
||||
| `recipientPublic`| `Uint8Array` | Recipient's 32-byte X25519 public key. |
|
||||
| `plaintext` | `Uint8Array` | Message bytes to encrypt. |
|
||||
| `nonce` | `Uint8Array` | 24-byte nonce; must be unique per (senderKey, recipientKey) pair. |
|
||||
|
||||
Returns the ciphertext with a 16-byte Poly1305 authentication tag appended.
|
||||
|
||||
**`open(senderPublic, recipientSecret, ciphertext, nonce): Uint8Array`**
|
||||
|
||||
| Parameter | Type | Description |
|
||||
|------------------|--------------|----------------------------------------------------------|
|
||||
| `senderPublic` | `Uint8Array` | Sender's 32-byte X25519 public key. |
|
||||
| `recipientSecret`| `Uint8Array` | Recipient's 32-byte X25519 secret key. |
|
||||
| `ciphertext` | `Uint8Array` | Ciphertext to decrypt (must include Poly1305 tag). |
|
||||
| `nonce` | `Uint8Array` | Same 24-byte nonce used during `seal`. |
|
||||
|
||||
Returns plaintext bytes. Throws `CryptoError` with code `AEAD.DECRYPT_FAILED` if authentication fails.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The shared secret produced by X25519 is not uniformly random — it is a compressed curve point. Passing it directly as a cipher key would be unsafe. HKDF-SHA256 with a fixed zero salt and the domain string `"suaveplan-auth-box-v1"` stretches and randomises the material to produce a 32-byte key suitable for XChaCha20-Poly1305. The domain string provides separation from the analogous sealed-box derivation that uses `"suaveplan-sealed-box-v1"`, preventing cross-protocol confusion if the same keypair is used with both constructs.
|
||||
|
||||
XChaCha20-Poly1305 is preferred over ChaCha20-Poly1305 because its 24-byte nonce makes collision probability negligible over a large volume of messages with the same keypair. Even so, nonce uniqueness is the caller's responsibility — the engine does not track or enforce it.
|
||||
@@ -0,0 +1,89 @@
|
||||
# CRYPTO_CODES
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
`codes.ts` defines the complete set of stable error codes used throughout `@suaveplan/crypto`. Each code is a namespaced string constant produced by `defineCodes` from `@suaveplan/error`, ensuring that codes are unique within the `"CRYPTO"` namespace and that the `CryptoCode` union type is always in sync with the registered values. Consumers can import `CRYPTO_CODES` and branch on `error.code` without inspecting message strings, making error handling robust across version upgrades.
|
||||
|
||||
## Features
|
||||
|
||||
- Seven codes covering the four main failure domains: key validity, AEAD nonce/decrypt, signing, and RNG
|
||||
- Generated via `defineCodes("CRYPTO", [...])` which prefixes each entry and freezes the resulting object
|
||||
- `CryptoCode` is derived as a `typeof` union so TypeScript narrows exhaustively in switch statements
|
||||
- No runtime dependencies beyond `@suaveplan/error`
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```typescript
|
||||
import { CRYPTO_CODES } from "@suaveplan/crypto-core/errors/codes";
|
||||
import { CryptoError } from "@suaveplan/crypto-core/errors/crypto-error";
|
||||
|
||||
throw new CryptoError(
|
||||
CRYPTO_CODES["RNG.UNAVAILABLE"],
|
||||
"error.crypto.rng.unavailable"
|
||||
);
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```typescript
|
||||
import { CRYPTO_CODES, type CryptoCode } from "@suaveplan/crypto-core/errors/codes";
|
||||
import { CryptoError } from "@suaveplan/crypto-core/errors/crypto-error";
|
||||
|
||||
function handleCryptoError(err: CryptoError): string {
|
||||
switch (err.code as CryptoCode) {
|
||||
case CRYPTO_CODES["AEAD.DECRYPT_FAILED"]:
|
||||
return "Decryption failed — message may be corrupted or the key is wrong.";
|
||||
case CRYPTO_CODES["SIGN.VERIFY_FAILED"]:
|
||||
return "Signature verification failed — message may have been tampered with.";
|
||||
case CRYPTO_CODES["KEY.INVALID_LENGTH"]:
|
||||
return "Supplied key has an incorrect byte length for this algorithm.";
|
||||
case CRYPTO_CODES["AEAD.INVALID_NONCE"]:
|
||||
return "Nonce does not meet the length requirements for the chosen cipher.";
|
||||
case CRYPTO_CODES["SIGN.INVALID_KEY"]:
|
||||
return "Signing key is malformed or does not correspond to the expected curve.";
|
||||
case CRYPTO_CODES["RNG.INVALID_LENGTH"]:
|
||||
return "Requested byte count is outside the permissible range.";
|
||||
case CRYPTO_CODES["RNG.UNAVAILABLE"]:
|
||||
return "Cryptographically secure random source is not available in this environment.";
|
||||
default:
|
||||
return "Unknown crypto error.";
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `CRYPTO_CODES`
|
||||
|
||||
```typescript
|
||||
const CRYPTO_CODES: Readonly<Record<string, string>>
|
||||
```
|
||||
|
||||
A frozen map from short code keys to their namespaced string values. All values are prefixed with `"CRYPTO."` by `defineCodes`.
|
||||
|
||||
| Key | Namespaced value | When thrown |
|
||||
|------------------------|-------------------------------|-------------------------------------------------------|
|
||||
| `KEY.INVALID_LENGTH` | `"CRYPTO.KEY.INVALID_LENGTH"` | A key byte length does not match algorithm requirements |
|
||||
| `AEAD.INVALID_NONCE` | `"CRYPTO.AEAD.INVALID_NONCE"` | Nonce byte length is wrong for the selected cipher |
|
||||
| `AEAD.DECRYPT_FAILED` | `"CRYPTO.AEAD.DECRYPT_FAILED"` | Authentication tag verification failed during decrypt |
|
||||
| `SIGN.INVALID_KEY` | `"CRYPTO.SIGN.INVALID_KEY"` | Signing key is malformed |
|
||||
| `SIGN.VERIFY_FAILED` | `"CRYPTO.SIGN.VERIFY_FAILED"` | Signature does not match the message |
|
||||
| `RNG.INVALID_LENGTH` | `"CRYPTO.RNG.INVALID_LENGTH"` | Byte count requested from RNG is invalid |
|
||||
| `RNG.UNAVAILABLE` | `"CRYPTO.RNG.UNAVAILABLE"` | Secure random source is absent (rare, environment issue) |
|
||||
|
||||
### `CryptoCode`
|
||||
|
||||
```typescript
|
||||
type CryptoCode = (typeof CRYPTO_CODES)[keyof typeof CRYPTO_CODES];
|
||||
```
|
||||
|
||||
Union of all namespaced code strings. Use this type to annotate `err.code` when narrowing `CryptoError` in typed catch blocks.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
`defineCodes` returns a plain frozen object. The `CryptoCode` type is derived with `typeof CRYPTO_CODES[keyof typeof CRYPTO_CODES]`, which collapses to a union of string literals. This approach means TypeScript will warn at compile time if a switch statement is missing a case when the union is used with `noUncheckedIndexedAccess` and strict narrowing enabled.
|
||||
|
||||
The `"AEAD.DECRYPT_FAILED"` code is reused for both authenticated-encryption failures and protocol-level failures (e.g. truncated sealed-box ciphertext), because both represent the same caller-facing outcome: the ciphertext could not be opened.
|
||||
@@ -0,0 +1,81 @@
|
||||
# CryptoError
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
`CryptoError` is the package-level error class for `@suaveplan/crypto`. It extends `SuaveplanError` with the `CryptoCode` discriminant, giving every thrown error a stable, namespaced code that consumers can branch on without parsing message strings. All encryption, decryption, signing, and key-agreement failures surface as `CryptoError` instances so catch handlers can distinguish crypto faults from unrelated infrastructure errors.
|
||||
|
||||
## Features
|
||||
|
||||
- Extends `SuaveplanError<CryptoCode>` for consistent error taxonomy across the `@suaveplan/*` ecosystem
|
||||
- Accepts the full `SuaveplanErrorOptions` bag (`cause`, `context`, `redact`) for cause-chain wrapping and structured context attachment
|
||||
- Constructor is `public`, overriding the `protected` base, so external engine adapters in sibling packages can throw typed `CryptoError` values directly
|
||||
- Compatible with OTel span-event adapters defined on `SuaveplanError`
|
||||
- Wire-serialisable via the standard `SuaveplanError` Zod schema
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```typescript
|
||||
import { CryptoError } from "@suaveplan/crypto-core/errors/crypto-error";
|
||||
import { CRYPTO_CODES } from "@suaveplan/crypto-core/errors/codes";
|
||||
|
||||
throw new CryptoError(
|
||||
CRYPTO_CODES["AEAD.DECRYPT_FAILED"],
|
||||
"error.crypto.aead.decrypt-failed"
|
||||
);
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```typescript
|
||||
import { CryptoError } from "@suaveplan/crypto-core/errors/crypto-error";
|
||||
import { CRYPTO_CODES } from "@suaveplan/crypto-core/errors/codes";
|
||||
|
||||
function decryptPayload(
|
||||
key: Uint8Array,
|
||||
nonce: Uint8Array,
|
||||
ciphertext: Uint8Array
|
||||
): Uint8Array {
|
||||
try {
|
||||
return lowLevelDecrypt(key, nonce, ciphertext);
|
||||
} catch (cause) {
|
||||
throw new CryptoError(
|
||||
CRYPTO_CODES["AEAD.DECRYPT_FAILED"],
|
||||
"error.crypto.aead.decrypt-failed",
|
||||
{
|
||||
cause,
|
||||
context: {
|
||||
ciphertextLength: ciphertext.length,
|
||||
nonceLength: nonce.length,
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `CryptoError`
|
||||
|
||||
```typescript
|
||||
class CryptoError extends SuaveplanError<CryptoCode>
|
||||
```
|
||||
|
||||
**Constructor**
|
||||
|
||||
| Parameter | Type | Description |
|
||||
|--------------|------------------------|----------------------------------------------------------|
|
||||
| `code` | `CryptoCode` | Stable namespaced error code from `CRYPTO_CODES`. |
|
||||
| `messageKey` | `string` | i18n resolver key (e.g. `"error.crypto.aead.decrypt-failed"`). |
|
||||
| `options` | `SuaveplanErrorOptions \| undefined` | Optional `cause`, `context`, and `redact` fields. |
|
||||
|
||||
All `SuaveplanError` instance methods are inherited, including `toJSON()`, `toWireFormat()`, and OTel helpers.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The constructor body is intentionally trivial — it exists solely to widen the `protected` base constructor to `public`. This pattern is required because `SuaveplanError`'s constructor is `protected` (it enforces that error classes always provide a discriminating `code`), but engine adapters outside this package's internal module graph need to throw `CryptoError` directly. The biome lint suppression comment is included to silence the `noUselessConstructor` rule, which would otherwise flag the no-op forwarding call as dead code.
|
||||
|
||||
The class does not add any new instance properties or methods on top of `SuaveplanError`. All richness — cause-chain, context bag, i18n, wire format — lives in the base class and flows through automatically.
|
||||
@@ -0,0 +1,72 @@
|
||||
# constant-time-equal
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
Compares two `Uint8Array` values in constant time to prevent timing side-channels. When comparing secrets such as authentication tags, HMAC outputs, or session tokens, a naive byte-by-byte comparison that returns early on the first mismatch leaks information about how many leading bytes matched — an attacker measuring response latency can exploit this to forge values one byte at a time. This implementation uses the XOR-accumulate pattern to ensure that branch-sensitive content (byte values) never causes an early return.
|
||||
|
||||
## Features
|
||||
|
||||
- Branch-free for content bytes — only the length check short-circuits (length is not secret in AEAD contexts)
|
||||
- O(min(len(a), len(b))) — no early exit on byte mismatch
|
||||
- Returns `true` only when lengths are equal AND all bytes match
|
||||
- Zero dependencies, pure TypeScript
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { constantTimeEqual } from "@suaveplan/crypto-core/primitives/constant-time-equal";
|
||||
|
||||
const tag1 = new Uint8Array([0x01, 0x02, 0x03]);
|
||||
const tag2 = new Uint8Array([0x01, 0x02, 0x03]);
|
||||
|
||||
constantTimeEqual(tag1, tag2); // true
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { constantTimeEqual } from "@suaveplan/crypto-core/primitives/constant-time-equal";
|
||||
import { aeadDecrypt, randomBytes } from "@suaveplan/crypto-core";
|
||||
|
||||
// Verify a server-generated token matches the expected token
|
||||
// without leaking information via timing differences
|
||||
function verifyToken(received: Uint8Array, expected: Uint8Array): boolean {
|
||||
return constantTimeEqual(received, expected);
|
||||
}
|
||||
|
||||
// Safe for use with auth tags, HMAC outputs, session tokens
|
||||
const a = new Uint8Array(32).fill(0xff);
|
||||
const b = new Uint8Array(32).fill(0xff);
|
||||
b[31] = 0xfe; // single-bit difference at the end
|
||||
|
||||
// Returns false, but execution time is the same as if all bytes matched
|
||||
verifyToken(a, b); // false
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `constantTimeEqual(a, b)`
|
||||
|
||||
```ts
|
||||
function constantTimeEqual(a: Uint8Array, b: Uint8Array): boolean
|
||||
```
|
||||
|
||||
Returns `true` iff `a.length === b.length` and every byte of `a` equals the corresponding byte of `b`.
|
||||
|
||||
| Param | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `a` | `Uint8Array` | First buffer. |
|
||||
| `b` | `Uint8Array` | Second buffer to compare against. |
|
||||
|
||||
**Returns:** `boolean` — `true` when buffers are identical, `false` otherwise.
|
||||
|
||||
**Does not throw.**
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The XOR-accumulate pattern (`diff |= a[i] ^ b[i]`) ensures the loop body never branches on byte values. At the end, `diff === 0` iff every XOR was zero (i.e., all bytes matched). The length check at the start does short-circuit, but buffer length is not considered secret in standard AEAD or HMAC comparison contexts — lengths are visible from ciphertext structure. If your protocol requires hiding lengths, pad the buffers before calling this function.
|
||||
|
||||
JavaScript's JIT may theoretically produce variable-timing code despite the branch-free pattern; production deployments should combine this with server-side rate limiting to bound oracle value.
|
||||
@@ -0,0 +1,97 @@
|
||||
# random-bytes
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
Provides the single authorised callsite for `crypto.getRandomValues` within `@suaveplan/crypto`. Every other module that needs random bytes calls `randomBytes()` or accepts a `ByteSource` injection — this design concentrates the randomness surface to one file, making it trivially auditable and testable. The `ByteSource` type replaces the old `Rng` alias (which conflicted with `@suaveplan/types`'s number-producing `Rng` interface) while preserving backwards compatibility via a deprecated re-export.
|
||||
|
||||
## Features
|
||||
|
||||
- `ByteSource = (n: number) => Uint8Array` — the injected randomness type used across key-generation functions
|
||||
- `Rng` deprecated alias for `ByteSource` for main-branch migration
|
||||
- `defaultByteSource` — calls `crypto.getRandomValues`; works in Bun, Node, and browsers
|
||||
- `defaultRng` — deprecated alias for `defaultByteSource`
|
||||
- `randomBytes(n, opts?)` — validated facade that rejects non-integer or negative `n`
|
||||
- `RandomBytesOptions.byteSource` for deterministic injection; `rng` kept as deprecated alias
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { randomBytes } from "@suaveplan/crypto-core/primitives/random-bytes";
|
||||
|
||||
const key = randomBytes(32); // 32 random bytes, CSPRNG-backed
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { randomBytes, type ByteSource } from "@suaveplan/crypto-core/primitives/random-bytes";
|
||||
|
||||
// Deterministic source for reproducible tests
|
||||
let counter = 0;
|
||||
const seeded: ByteSource = (n) => {
|
||||
const out = new Uint8Array(n);
|
||||
for (let i = 0; i < n; i++) out[i] = (counter++ & 0xff);
|
||||
return out;
|
||||
};
|
||||
|
||||
const key = randomBytes(32, { byteSource: seeded });
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `ByteSource`
|
||||
|
||||
```ts
|
||||
type ByteSource = (n: number) => Uint8Array
|
||||
```
|
||||
|
||||
A function that returns `n` cryptographically-suitable bytes. The default implementation calls `crypto.getRandomValues`.
|
||||
|
||||
### `Rng` (deprecated)
|
||||
|
||||
```ts
|
||||
/** @deprecated */ type Rng = ByteSource
|
||||
```
|
||||
|
||||
Backwards-compatibility alias. Prefer `ByteSource` in new code.
|
||||
|
||||
### `defaultByteSource`
|
||||
|
||||
```ts
|
||||
const defaultByteSource: ByteSource
|
||||
```
|
||||
|
||||
The production byte source. Wraps `crypto.getRandomValues`. Works in Bun, Node ≥18, and browsers.
|
||||
|
||||
### `defaultRng` (deprecated)
|
||||
|
||||
```ts
|
||||
/** @deprecated */ const defaultRng: ByteSource
|
||||
```
|
||||
|
||||
Alias for `defaultByteSource`.
|
||||
|
||||
### `randomBytes(n, options?)`
|
||||
|
||||
```ts
|
||||
function randomBytes(n: number, options?: RandomBytesOptions): Uint8Array
|
||||
```
|
||||
|
||||
Returns `n` random bytes. Throws `CryptoError("CRYPTO.RNG.INVALID_LENGTH")` if `n` is not a non-negative integer.
|
||||
|
||||
| Param | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `n` | `number` | Number of bytes to generate. Must be a non-negative integer. |
|
||||
| `options.byteSource` | `ByteSource?` | Override the default source. |
|
||||
| `options.rng` | `ByteSource?` | Deprecated alias for `byteSource`. |
|
||||
|
||||
**Throws:** `CryptoError("CRYPTO.RNG.INVALID_LENGTH")` — `n` is negative, non-integer, or NaN.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The `crypto.getRandomValues` restriction is enforced at code review and by `tests/no-disallowed.test.ts`, which walks every `.ts` source file under `src/` and asserts that `crypto.getRandomValues` appears only in this file. This guards against accidental randomness leakage in library code that should be injecting `ByteSource` for testability.
|
||||
|
||||
The `ByteSource` / `Rng` rename was necessary because `@suaveplan/types` exports `Rng = { nextUint32(): number; nextFloat(): number }` — a fundamentally different interface. Shipping both would cause confusing type errors for consumers who import from both packages.
|
||||
@@ -0,0 +1,68 @@
|
||||
# zeroize
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
Overwrites a `Uint8Array` buffer with zeros in-place to minimise the lifetime of sensitive data on the JavaScript heap. Callers should invoke `zeroize` as soon as a secret key, derived key, or plaintext is no longer needed. While JavaScript's garbage collector may have already relocated the backing `ArrayBuffer` to a new memory region — making this a best-effort operation — it is still considered good practice because it reduces the window during which a memory-inspection attack or heap dump could expose the sensitive bytes.
|
||||
|
||||
## Features
|
||||
|
||||
- Fills every byte of the buffer with `0x00` using `Uint8Array.fill`
|
||||
- In-place mutation — no allocation, no return value
|
||||
- Works on any `Uint8Array` subview regardless of byte offset or length
|
||||
- Zero dependencies
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { zeroize } from "@suaveplan/crypto-core/primitives/zeroize";
|
||||
|
||||
const secretKey = new Uint8Array(32);
|
||||
// ... use secretKey for cryptographic operations ...
|
||||
|
||||
zeroize(secretKey); // clear sensitive bytes
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { zeroize } from "@suaveplan/crypto-core/primitives/zeroize";
|
||||
import { randomBytes } from "@suaveplan/crypto-core/primitives/random-bytes";
|
||||
import { aeadEncrypt } from "@suaveplan/crypto-core/aead";
|
||||
|
||||
function encryptAndForget(plaintext: Uint8Array): Uint8Array {
|
||||
const key = randomBytes(32);
|
||||
const nonce = randomBytes(12);
|
||||
try {
|
||||
return aeadEncrypt("AES-256-GCM", key, plaintext, { nonce });
|
||||
} finally {
|
||||
// Always clear keys even if encrypt throws
|
||||
zeroize(key);
|
||||
zeroize(nonce);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `zeroize(buf)`
|
||||
|
||||
```ts
|
||||
function zeroize(buf: Uint8Array): void
|
||||
```
|
||||
|
||||
Fills every byte of `buf` with `0x00` in-place.
|
||||
|
||||
| Param | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `buf` | `Uint8Array` | Buffer to zero out. Modified in-place. |
|
||||
|
||||
**Returns:** `void`
|
||||
|
||||
**Does not throw.** Calling on a zero-length buffer is a no-op.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
`Uint8Array.fill(0)` is the most direct approach. Some runtimes optimise this to a `memset` call at the C level, which is typically not subject to compiler dead-store elimination. However, JavaScript engines may still optimise away stores to unreachable buffers — this is a known limitation of in-process zeroing in managed runtimes. For high-security environments where in-process zeroing is insufficient, consider using hardware security modules or OS-level secure memory (not available from pure JavaScript).
|
||||
@@ -0,0 +1,117 @@
|
||||
# NobleSealedBoxEngine
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
`NobleSealedBoxEngine` implements the `SealedBoxEngine` interface using a `crypto_box_seal`-style anonymous-sender encryption protocol. The sender generates a fresh ephemeral X25519 keypair per message, performs ECDH with the recipient's long-term public key, derives a symmetric key via HKDF-SHA256, and encrypts with XChaCha20-Poly1305. The ephemeral public key and nonce are prepended to the output so the recipient can recover the shared secret without any out-of-band communication. Sender anonymity is preserved because the ephemeral secret key is discarded immediately after sealing.
|
||||
|
||||
## Features
|
||||
|
||||
- Implements `SealedBoxEngine` (`seal` / `open`) for anonymous authenticated encryption
|
||||
- Protocol output layout: `ephemeralPub (32) || nonce (24) || ciphertext || tag (16)`
|
||||
- HKDF-SHA256 key derivation with zero salt and info `"suaveplan-sealed-box-v1"` for domain separation
|
||||
- Ephemeral keypair generated fresh per `seal` call using `defaultByteSource` (secure random)
|
||||
- Minimum ciphertext length validated during `open`; short ciphertexts throw `CryptoError` before attempting decryption
|
||||
- All decryption failures wrapped in `CryptoError` with code `AEAD.DECRYPT_FAILED`
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```typescript
|
||||
import { NobleSealedBoxEngine } from "@suaveplan/crypto-core/sealed-box/noble-sealed-box-engine";
|
||||
import { NobleX25519Engine } from "@suaveplan/crypto-core/agree/noble-agree-engine";
|
||||
|
||||
const agreeEngine = new NobleX25519Engine();
|
||||
const sealedBoxEngine = new NobleSealedBoxEngine();
|
||||
|
||||
const recipient = agreeEngine.generateKeypair();
|
||||
|
||||
// Any party can seal to recipient without knowing sender's identity
|
||||
const plaintext = new TextEncoder().encode("anonymous message");
|
||||
const ciphertext = sealedBoxEngine.seal(recipient.publicKey, plaintext);
|
||||
|
||||
// Only recipient can open
|
||||
const recovered = sealedBoxEngine.open(
|
||||
recipient.secretKey,
|
||||
recipient.publicKey,
|
||||
ciphertext
|
||||
);
|
||||
|
||||
console.log(new TextDecoder().decode(recovered)); // "anonymous message"
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```typescript
|
||||
import { NobleSealedBoxEngine } from "@suaveplan/crypto-core/sealed-box/noble-sealed-box-engine";
|
||||
import { NobleX25519Engine } from "@suaveplan/crypto-core/agree/noble-agree-engine";
|
||||
import { CryptoError } from "@suaveplan/crypto-core/errors/crypto-error";
|
||||
import { CRYPTO_CODES } from "@suaveplan/crypto-core/errors/codes";
|
||||
|
||||
const agreeEngine = new NobleX25519Engine();
|
||||
const engine = new NobleSealedBoxEngine();
|
||||
const recipient = agreeEngine.generateKeypair();
|
||||
|
||||
// Each seal call produces a different ciphertext (fresh ephemeral key + nonce)
|
||||
const msg = new TextEncoder().encode("one-time sealed value");
|
||||
const ct1 = engine.seal(recipient.publicKey, msg);
|
||||
const ct2 = engine.seal(recipient.publicKey, msg);
|
||||
console.log(ct1.length === ct2.length); // true (same size)
|
||||
|
||||
// Truncated ciphertext is caught before decryption attempt
|
||||
try {
|
||||
engine.open(recipient.secretKey, recipient.publicKey, new Uint8Array(10));
|
||||
} catch (err) {
|
||||
if (err instanceof CryptoError && err.code === CRYPTO_CODES["AEAD.DECRYPT_FAILED"]) {
|
||||
console.log("Short ciphertext rejected early");
|
||||
}
|
||||
}
|
||||
|
||||
// Tampered ciphertext also fails at authentication
|
||||
try {
|
||||
const tampered = ct1.slice();
|
||||
tampered[40] ^= 0x01;
|
||||
engine.open(recipient.secretKey, recipient.publicKey, tampered);
|
||||
} catch (err) {
|
||||
if (err instanceof CryptoError && err.code === CRYPTO_CODES["AEAD.DECRYPT_FAILED"]) {
|
||||
console.log("Tampered ciphertext rejected");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `NobleSealedBoxEngine`
|
||||
|
||||
```typescript
|
||||
class NobleSealedBoxEngine implements SealedBoxEngine
|
||||
```
|
||||
|
||||
**`seal(recipientPublic, plaintext): Uint8Array`**
|
||||
|
||||
| Parameter | Type | Description |
|
||||
|-------------------|--------------|--------------------------------------------------------|
|
||||
| `recipientPublic` | `Uint8Array` | Recipient's 32-byte X25519 public key. |
|
||||
| `plaintext` | `Uint8Array` | Message bytes to encrypt. |
|
||||
|
||||
Returns a self-contained blob: `ephemeralPublicKey (32) || nonce (24) || ciphertext || poly1305Tag (16)`. Total overhead is 72 bytes beyond plaintext length.
|
||||
|
||||
**`open(recipientSecret, recipientPublic, ciphertext): Uint8Array`**
|
||||
|
||||
| Parameter | Type | Description |
|
||||
|-------------------|--------------|----------------------------------------------------------------|
|
||||
| `recipientSecret` | `Uint8Array` | Recipient's 32-byte X25519 secret key. |
|
||||
| `recipientPublic` | `Uint8Array` | Recipient's 32-byte X25519 public key (currently unused but kept for API symmetry). |
|
||||
| `ciphertext` | `Uint8Array` | Sealed blob produced by `seal`. |
|
||||
|
||||
Returns plaintext bytes. Throws `CryptoError` (`AEAD.DECRYPT_FAILED`) if:
|
||||
|
||||
- The ciphertext is shorter than 72 bytes (minimum: 32 ephKey + 24 nonce + 16 tag)
|
||||
- Authentication tag verification fails
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The `_recipientPublic` parameter in `open` is accepted to keep the `SealedBoxEngine` interface consistent with engines that do require it (e.g. the libsodium adapter), but is not used in the computation — the recipient public key is only needed to reconstruct the shared secret on the _sender_ side. The Noble implementation derives everything from `recipientSecret` and the extracted `ephemeralPublic` alone.
|
||||
|
||||
Each `seal` call draws fresh randomness for both the ephemeral secret (32 bytes) and the XChaCha20-Poly1305 nonce (24 bytes) from `defaultByteSource`. This means two invocations with identical plaintext and recipient key will always produce different ciphertexts — forward-secrecy is automatic. The trade-off is that the nonce does not need to be managed by the caller for this primitive, unlike `authBox` where nonce uniqueness is the caller's responsibility.
|
||||
@@ -0,0 +1,94 @@
|
||||
# sealed-box
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
- [noble-sealed-box-engine](./noble-sealed-box-engine/noble-sealed-box-engine.md) — <!-- begin:auto-docs --> _(no submodules)_- [sealed-box](./sealed-box.md) — <!-- begin:auto-docs --> _(no submodules)_- noble-sealed-box-engine — <!-- begin:auto-docs --> _…
|
||||
- [noble-sealed-box-engine](./noble-sealed-box-engine/noble-sealed-box-engine.md) — <!-- begin:auto-docs --> _(no submodules)_- [sealed-box](./sealed-box.md) — <!-- begin:auto-docs --> _(no submodules)_- [noble-sealed-box-engine](./noble-sealed-box-engine/noble-sealed-box-engine.md) — <!-- begin:auto-docs --> _(no submodules)_- [sealed-box](./sealed-box.md) — <!-- begin:auto-docs --> _(no submodules)_- noble-sealed-box-engine — <!…
|
||||
- [noble-sealed-box-engine](./noble-sealed-box-engine/noble-sealed-box-engine.md) — <!-- begin:auto-docs --> _(no submodules)_- [sealed-box](./sealed-box.md) — <!-- begin:auto-docs --> _(no submodules)_- noble-sealed-box-engine — <!-- begin:auto-docs -->…
|
||||
- [noble-sealed-box-engine](./noble-sealed-box-engine/noble-sealed-box-engine.md) — <!-- begin:auto-docs --> _(no submodules)_- [sealed-box](./sealed-box.md) — <!-- begin:auto-docs --> _(no submodules)_
|
||||
## Purpose
|
||||
|
||||
Provides anonymous-sender sealed-box encryption following `crypto_box_seal` semantics. The sender generates an ephemeral X25519 keypair, performs ECDH with the recipient's public key, derives a symmetric key via HKDF-SHA256, then encrypts with XChaCha20-Poly1305. The ephemeral public key is prepended to the ciphertext blob so the recipient can derive the same symmetric key without the sender needing to reveal their identity. This pattern is commonly used for one-way encrypted messages where sender anonymity is desired.
|
||||
|
||||
## Features
|
||||
|
||||
- Anonymous sender — no sender keypair required
|
||||
- X25519 ECDH + HKDF-SHA256 + XChaCha20-Poly1305 (crypto_box_seal semantics)
|
||||
- Self-contained output: `ephemeralPub (32) || nonce (24) || ciphertext || tag (16)`
|
||||
- Engine-pluggable via `SealedBoxEngine` interface
|
||||
- Authenticated decryption — rejects tampered ciphertext with `CRYPTO.AEAD.DECRYPT_FAILED`
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { sealedBox } from "@suaveplan/crypto-core";
|
||||
import { x25519 } from "@suaveplan/crypto-core";
|
||||
|
||||
const recipient = x25519.generateKeypair();
|
||||
|
||||
// Seal (any sender, no keypair needed)
|
||||
const ciphertext = sealedBox.seal(recipient.publicKey, new TextEncoder().encode("secret"));
|
||||
|
||||
// Open (recipient only)
|
||||
const plaintext = sealedBox.open(recipient.secretKey, recipient.publicKey, ciphertext);
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { sealedBox } from "@suaveplan/crypto-core";
|
||||
import { x25519 } from "@suaveplan/crypto-core";
|
||||
import type { SealedBoxEngine } from "@suaveplan/crypto-core/engines";
|
||||
|
||||
// Custom engine for testing (deterministic, not secure)
|
||||
const stubEngine: SealedBoxEngine = {
|
||||
seal: (_pub, pt) => pt.slice(),
|
||||
open: (_sec, _pub, ct) => ct.slice(),
|
||||
};
|
||||
|
||||
const recipient = x25519.generateKeypair();
|
||||
const msg = new Uint8Array([1, 2, 3]);
|
||||
|
||||
const ct = sealedBox.seal(recipient.publicKey, msg, { engine: stubEngine });
|
||||
const pt = sealedBox.open(recipient.secretKey, recipient.publicKey, ct, { engine: stubEngine });
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `seal(recipientPublic, plaintext, opts?)`
|
||||
|
||||
```ts
|
||||
function seal(
|
||||
recipientPublic: Uint8Array,
|
||||
plaintext: Uint8Array,
|
||||
opts?: SealedBoxEngineOptions
|
||||
): Uint8Array
|
||||
```
|
||||
|
||||
Encrypts and authenticates `plaintext` for the recipient. Returns a self-contained blob containing the ephemeral public key, nonce, ciphertext, and authentication tag.
|
||||
|
||||
| Param | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `recipientPublic` | `Uint8Array` | Recipient's X25519 public key (32 bytes). |
|
||||
| `plaintext` | `Uint8Array` | Data to encrypt. |
|
||||
| `opts.engine` | `SealedBoxEngine?` | Override the default noble engine. |
|
||||
|
||||
### `open(recipientSecret, recipientPublic, ciphertext, opts?)`
|
||||
|
||||
```ts
|
||||
function open(
|
||||
recipientSecret: Uint8Array,
|
||||
recipientPublic: Uint8Array,
|
||||
ciphertext: Uint8Array,
|
||||
opts?: SealedBoxEngineOptions
|
||||
): Uint8Array
|
||||
```
|
||||
|
||||
Decrypts a sealed box. Throws on authentication failure.
|
||||
|
||||
**Throws:** `CryptoError("CRYPTO.AEAD.DECRYPT_FAILED")` — ciphertext is too short, or authentication tag verification fails.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The HKDF info string `"suaveplan-sealed-box-v1"` is domain-separated to prevent key reuse across protocols. A zero salt is used because the X25519 shared secret already provides sufficient entropy for HKDF extraction. The nonce is randomly generated per seal operation and stored in the ciphertext blob, so the same key can be reused across many messages without nonce-reuse risk. The 24-byte XChaCha20-Poly1305 nonce is large enough that random generation is safe for up to 2^96 messages per recipient key before birthday collisions become plausible.
|
||||
@@ -0,0 +1,89 @@
|
||||
# ecdsa-p256
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
Provides ECDSA-P256 digital signature operations — key generation, signing, and verification — backed by `@noble/curves`. ECDSA over NIST P-256 (secp256r1) is widely deployed in TLS certificates, WebAuthn, and enterprise PKI infrastructure. Signatures are computed over the SHA-256 digest of the message and encoded in compact (r||s) 64-byte form. The facade is engine-pluggable: pass `{ engine }` to substitute a WebCrypto-backed implementation that may leverage hardware acceleration.
|
||||
|
||||
## Features
|
||||
|
||||
- Key generation with P-256 scalar validation retry loop (not all 32-byte values are valid scalars)
|
||||
- Signs over SHA-256 digest — callers do not need to pre-hash
|
||||
- Compact 64-byte (r||s) signature format
|
||||
- Verification returns `boolean`, never throws on malformed input
|
||||
- Engine-pluggable via `SignEngine` interface
|
||||
- `ByteSource` / deprecated `rng` option for key-generation injection
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { ecdsaP256 } from "@suaveplan/crypto-core";
|
||||
|
||||
const keypair = ecdsaP256.generateKeypair();
|
||||
const message = new TextEncoder().encode("document content");
|
||||
const sig = ecdsaP256.sign(keypair.secretKey, message);
|
||||
const ok = ecdsaP256.verify(keypair.publicKey, sig, message);
|
||||
// ok === true
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { ecdsaP256 } from "@suaveplan/crypto-core";
|
||||
import type { ByteSource } from "@suaveplan/crypto-core/primitives/random-bytes";
|
||||
|
||||
// Deterministic key generation for testing
|
||||
const seeded: ByteSource = (() => {
|
||||
let v = 1;
|
||||
return (n) => {
|
||||
const out = new Uint8Array(n);
|
||||
for (let i = 0; i < n; i++) out[i] = (v++ & 0xff);
|
||||
return out;
|
||||
};
|
||||
})();
|
||||
|
||||
const kp = ecdsaP256.generateKeypair({ byteSource: seeded });
|
||||
const msg = new Uint8Array([0xde, 0xad]);
|
||||
const sig = ecdsaP256.sign(kp.secretKey, msg);
|
||||
console.log(ecdsaP256.verify(kp.publicKey, sig, msg)); // true
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `EcdsaP256Keypair`
|
||||
|
||||
```ts
|
||||
type EcdsaP256Keypair = { publicKey: Uint8Array; secretKey: Uint8Array }
|
||||
```
|
||||
|
||||
Public key is a 33-byte compressed point; secret key is a 32-byte scalar.
|
||||
|
||||
### `generateKeypair(opts?)`
|
||||
|
||||
```ts
|
||||
function generateKeypair(opts?: EcdsaP256Options): EcdsaP256Keypair
|
||||
```
|
||||
|
||||
Generates a fresh keypair. Retries key generation until the random bytes form a valid P-256 private scalar (probability of retry is negligible — about 2^-128).
|
||||
|
||||
### `sign(secretKey, message, opts?)`
|
||||
|
||||
```ts
|
||||
function sign(secretKey: Uint8Array, message: Uint8Array, opts?: { engine?: SignEngine }): Uint8Array
|
||||
```
|
||||
|
||||
Signs `SHA-256(message)` with `secretKey`. Returns a compact 64-byte (r||s) signature.
|
||||
|
||||
### `verify(publicKey, signature, message, opts?)`
|
||||
|
||||
```ts
|
||||
function verify(publicKey: Uint8Array, signature: Uint8Array, message: Uint8Array, opts?: { engine?: SignEngine }): boolean
|
||||
```
|
||||
|
||||
Verifies compact signature against `SHA-256(message)`. Returns `false` (never throws) on malformed inputs.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
P-256 private key generation requires checking that the random scalar is in `[1, n-1]` where `n` is the curve order. The probability that a uniformly random 32-byte value is out of range is approximately `1 - n/2^256 ≈ 2^-128`, so the retry loop terminates in one iteration with overwhelming probability. The SHA-256 pre-hash is applied by the facade (not the noble library's internal hashing) to keep the engine interface consistent with Ed25519 where the full message is hashed internally. Compact signature format omits the DER encoding overhead and is interoperable with most modern P-256 consumers.
|
||||
@@ -0,0 +1,92 @@
|
||||
# ed25519
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
Provides Ed25519 digital signature operations — key generation, signing, and verification — backed by `@noble/curves`. Ed25519 is a high-speed elliptic curve signature algorithm over Curve25519, offering 128-bit security with compact 64-byte signatures and 32-byte keys. It is deterministic (no per-signature randomness needed) and is the recommended choice for new protocols. The facade is engine-pluggable: pass `{ engine }` to substitute a WebCrypto or libsodium-backed implementation.
|
||||
|
||||
## Features
|
||||
|
||||
- Key generation using injected `ByteSource` (testable) or `crypto.getRandomValues` (production)
|
||||
- Deterministic signing — same key + message always produces the same signature
|
||||
- Verification returns `boolean`, never throws on malformed input
|
||||
- Engine-pluggable via `SignEngine` interface
|
||||
- `ByteSource` / deprecated `rng` option for key-generation injection
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { ed25519 } from "@suaveplan/crypto-core";
|
||||
|
||||
const keypair = ed25519.generateKeypair();
|
||||
const message = new TextEncoder().encode("hello");
|
||||
const sig = ed25519.sign(keypair.secretKey, message);
|
||||
const ok = ed25519.verify(keypair.publicKey, sig, message);
|
||||
// ok === true
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { ed25519 } from "@suaveplan/crypto-core";
|
||||
import type { SignEngine } from "@suaveplan/crypto-core/engines";
|
||||
import type { ByteSource } from "@suaveplan/crypto-core/primitives/random-bytes";
|
||||
|
||||
// Deterministic key generation for reproducible tests
|
||||
let seed = 0;
|
||||
const seeded: ByteSource = (n) => {
|
||||
const out = new Uint8Array(n);
|
||||
for (let i = 0; i < n; i++) out[i] = (seed++ & 0xff);
|
||||
return out;
|
||||
};
|
||||
|
||||
const kp = ed25519.generateKeypair({ byteSource: seeded });
|
||||
const msg = new Uint8Array([1, 2, 3]);
|
||||
const sig = ed25519.sign(kp.secretKey, msg);
|
||||
console.log(ed25519.verify(kp.publicKey, sig, msg)); // true
|
||||
console.log(ed25519.verify(kp.publicKey, sig, new Uint8Array([99]))); // false
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `Ed25519Keypair`
|
||||
|
||||
```ts
|
||||
type Ed25519Keypair = { publicKey: Uint8Array; secretKey: Uint8Array }
|
||||
```
|
||||
|
||||
### `generateKeypair(opts?)`
|
||||
|
||||
```ts
|
||||
function generateKeypair(opts?: Ed25519Options): Ed25519Keypair
|
||||
```
|
||||
|
||||
Generates a fresh keypair. Every 32-byte value is a valid Ed25519 secret key, so no retry loop is needed.
|
||||
|
||||
| Param | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `opts.byteSource` | `ByteSource?` | Byte source for key material. |
|
||||
| `opts.rng` | `ByteSource?` | Deprecated alias for `byteSource`. |
|
||||
| `opts.engine` | `SignEngine?` | Override the default noble engine. |
|
||||
|
||||
### `sign(secretKey, message, opts?)`
|
||||
|
||||
```ts
|
||||
function sign(secretKey: Uint8Array, message: Uint8Array, opts?: { engine?: SignEngine }): Uint8Array
|
||||
```
|
||||
|
||||
Signs `message` with `secretKey`. Returns a 64-byte signature.
|
||||
|
||||
### `verify(publicKey, signature, message, opts?)`
|
||||
|
||||
```ts
|
||||
function verify(publicKey: Uint8Array, signature: Uint8Array, message: Uint8Array, opts?: { engine?: SignEngine }): boolean
|
||||
```
|
||||
|
||||
Verifies `signature` over `message` against `publicKey`. Returns `false` (never throws) on malformed inputs.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
Ed25519 uses the Ristretto255 group internally in some implementations; `@noble/curves` uses the standard Edwards25519 form. The 64-byte signature is the standard (R, S) encoding where R is a compressed point and S is a scalar. Noble's `verify` returns `false` for many malformed inputs that other libraries throw on — the facade wraps the call in a try/catch to guarantee boolean-only returns from all implementations.
|
||||
@@ -0,0 +1,117 @@
|
||||
# NobleEd25519Engine / NobleEcdsaP256Engine
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
This module provides two `SignEngine` implementations for the `@suaveplan/crypto` facade: `NobleEd25519Engine` for Ed25519 and `NobleEcdsaP256Engine` for ECDSA-P256. Both delegate through `@suaveplan/crypto-curve` subpaths, ensuring the build-time primitive selector in `crypto.config.ts` applies uniformly. The engines are consumed by the `ed25519Sign`, `ed25519Verify`, `ecdsaP256Sign`, and `ecdsaP256Verify` facade functions, and can be supplied directly via engine options for testing or custom engine composition.
|
||||
|
||||
## Features
|
||||
|
||||
- `NobleEd25519Engine`: implements `SignEngine` for Ed25519 (64-byte signatures, 32-byte keys)
|
||||
- `NobleEcdsaP256Engine`: implements `SignEngine` for ECDSA-P256 (compact 64-byte r‖s signatures over SHA-256 digests)
|
||||
- Both accept an optional `ByteSource` in `generateKeypair` for deterministic test key generation
|
||||
- Ed25519 key generation needs no retry loop — every 32-byte value is a valid secret
|
||||
- P-256 key generation uses `p256.randomPrivateKey()` which internally handles the rejection-sampling retry; injected byte sources bypass this and must supply a valid scalar
|
||||
- ECDSA signs over the SHA-256 digest of the message, so callers do not need to pre-hash
|
||||
- Delegates through `@suaveplan/crypto-curve/ed25519` and `@suaveplan/crypto-curve/p256`
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```typescript
|
||||
import { NobleEd25519Engine } from "@suaveplan/crypto-core/sign/noble-sign-engine";
|
||||
|
||||
const engine = new NobleEd25519Engine();
|
||||
const keypair = engine.generateKeypair();
|
||||
|
||||
const message = new TextEncoder().encode("hello world");
|
||||
const signature = engine.sign(keypair.secretKey, message);
|
||||
const valid = engine.verify(keypair.publicKey, signature, message);
|
||||
|
||||
console.log(valid); // true
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```typescript
|
||||
import {
|
||||
NobleEd25519Engine,
|
||||
NobleEcdsaP256Engine,
|
||||
} from "@suaveplan/crypto-core/sign/noble-sign-engine";
|
||||
|
||||
// Ed25519 with a deterministic byte source
|
||||
const fixedSeed = (n: number) => new Uint8Array(n).fill(0x42);
|
||||
const ed25519Engine = new NobleEd25519Engine();
|
||||
const deterministicKeypair = ed25519Engine.generateKeypair(fixedSeed);
|
||||
|
||||
// ECDSA-P256 — signs over SHA-256 digest internally
|
||||
const p256Engine = new NobleEcdsaP256Engine();
|
||||
const p256Keypair = p256Engine.generateKeypair();
|
||||
|
||||
const message = new TextEncoder().encode("transaction payload");
|
||||
const sig256 = p256Engine.sign(p256Keypair.secretKey, message);
|
||||
|
||||
// Verification returns boolean — does not throw on invalid signatures
|
||||
const ok = p256Engine.verify(p256Keypair.publicKey, sig256, message);
|
||||
console.log(ok); // true
|
||||
|
||||
// Cross-engine verification fails gracefully
|
||||
const wrongSig = ed25519Engine.sign(deterministicKeypair.secretKey, message);
|
||||
const crossOk = p256Engine.verify(p256Keypair.publicKey, wrongSig, message);
|
||||
console.log(crossOk); // false
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `NobleEd25519Engine`
|
||||
|
||||
```typescript
|
||||
class NobleEd25519Engine implements SignEngine
|
||||
```
|
||||
|
||||
**`generateKeypair(byteSource?): SignKeypair`**
|
||||
|
||||
| Parameter | Type | Description |
|
||||
|--------------|-------------------------|-----------------------------------------------------------------|
|
||||
| `byteSource` | `ByteSource \| undefined` | Optional random byte source. When omitted, uses `ed25519.randomPrivateKey()`. |
|
||||
|
||||
Returns `SignKeypair { publicKey: Uint8Array; secretKey: Uint8Array }` (both 32 bytes).
|
||||
|
||||
**`sign(secretKey, message): Uint8Array`**
|
||||
|
||||
Signs `message` directly (Ed25519 is a pure signature scheme — no pre-hashing). Returns a 64-byte signature.
|
||||
|
||||
**`verify(publicKey, signature, message): boolean`**
|
||||
|
||||
Returns `true` if the signature is valid for the message and public key. Never throws on an invalid signature — returns `false` instead.
|
||||
|
||||
---
|
||||
|
||||
### `NobleEcdsaP256Engine`
|
||||
|
||||
```typescript
|
||||
class NobleEcdsaP256Engine implements SignEngine
|
||||
```
|
||||
|
||||
**`generateKeypair(byteSource?): SignKeypair`**
|
||||
|
||||
| Parameter | Type | Description |
|
||||
|--------------|-------------------------|------------------------------------------------------------------------------|
|
||||
| `byteSource` | `ByteSource \| undefined` | Optional byte source. When omitted, `p256.randomPrivateKey()` handles rejection sampling. When supplied, the 32 bytes are used directly and must form a valid P-256 scalar. |
|
||||
|
||||
Returns `SignKeypair { publicKey: Uint8Array; secretKey: Uint8Array }`. The public key is 33-byte compressed form.
|
||||
|
||||
**`sign(secretKey, message): Uint8Array`**
|
||||
|
||||
Hashes `message` with SHA-256, then signs the digest. Returns a compact 64-byte signature (r‖s, without DER encoding).
|
||||
|
||||
**`verify(publicKey, signature, message): boolean`**
|
||||
|
||||
Hashes `message` with SHA-256 and verifies the compact signature. Returns `true` on success, `false` on any verification failure.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
Ed25519 and P-256 differ in their key-space properties. Ed25519 uses a 32-byte seed that is internally hashed to produce the actual scalar, so every seed value is valid. P-256 requires the private scalar to be in the range `[1, n-1]` where `n` is the curve order, which excludes a negligibly small fraction of 32-byte values. The `p256.randomPrivateKey()` function handles rejection-sampling internally; however, when a `byteSource` is injected (for testing), the engine passes the bytes directly without rejection-sampling, placing the validity constraint on the caller.
|
||||
|
||||
ECDSA-P256 signs over the SHA-256 digest of the message. This pre-hashing is performed inside the engine rather than in the facade so that all code paths — including direct engine use — apply the hash consistently. The `@noble/curves` P-256 `sign` function also accepts a `lowS` option (enforced by default in newer versions) to produce canonical low-S signatures, which improves compatibility with verification libraries that reject high-S signatures.
|
||||
Reference in New Issue
Block a user