first commit
This commit is contained in:
14
docs/security-stack/crypto/crypto-utils/CHANGELOG.md
Normal file
14
docs/security-stack/crypto/crypto-utils/CHANGELOG.md
Normal file
@@ -0,0 +1,14 @@
|
||||
# @suaveplan/crypto-utils
|
||||
|
||||
## 0.0.0
|
||||
|
||||
- Initial scaffold under `crypto-primitive-replaceability` Phase 0.
|
||||
- Encoding: hex, base64, base64url, base58.
|
||||
- Bytes helpers: `concatBytes`, `xor`, `i2osp`, `os2ip`.
|
||||
- Constant-time equality.
|
||||
- Random bytes (wraps `globalThis.crypto.getRandomValues`).
|
||||
- Zeroize.
|
||||
- Merkle-root / Merkle-proof (generic over any hash primitive).
|
||||
- Test harnesses: `runVectors` (KAT), `diffFuzz` (differential fuzz).
|
||||
- `PrimitiveId` union + registry of every primitive across `crypto-hash`, `crypto-curve`, `crypto-cipher`.
|
||||
- `PrimitiveNotImplemented` error (code `CRYPTO.UTILS.PRIMITIVE_NOT_IMPLEMENTED`).
|
||||
65
docs/security-stack/crypto/crypto-utils/README.md
Normal file
65
docs/security-stack/crypto/crypto-utils/README.md
Normal file
File diff suppressed because one or more lines are too long
@@ -0,0 +1,50 @@
|
||||
# concatBytes
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
Concatenate any number of `Uint8Array` buffers into a single freshly-allocated result. Used extensively by crypto protocols that frame authenticated data by length-prefix or that split a plaintext across record boundaries. This module re-exports `concatUint8Arrays` from `@suaveplan/bytes` under the crypto-friendly name `concatBytes`, centralising the helper so every downstream cipher, MAC, or encoding never has to handwrite the `set`-at-offset loop and all packages share a single, tested implementation.
|
||||
|
||||
## Features
|
||||
|
||||
- Variadic: accepts any number of parts (including zero).
|
||||
- Returns a new `Uint8Array` even for single-arg calls (never aliases an input).
|
||||
- Delegates to `@suaveplan/bytes` — one implementation, zero duplication.
|
||||
- Zero implementation code in this module — pure delegation.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { concatBytes } from "@suaveplan/crypto-utils/bytes";
|
||||
|
||||
const frame = concatBytes(header, payload, footer);
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { concatBytes } from "@suaveplan/crypto-utils/bytes";
|
||||
|
||||
// Build a TLS-style length-prefixed message.
|
||||
function frame(...parts: Uint8Array[]): Uint8Array {
|
||||
const body = concatBytes(...parts);
|
||||
const lenPrefix = new Uint8Array([(body.length >> 8) & 0xff, body.length & 0xff]);
|
||||
return concatBytes(lenPrefix, body);
|
||||
}
|
||||
|
||||
const out = frame(new Uint8Array([0x01]), new Uint8Array([0x02, 0x03]));
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `concatBytes(...parts: Uint8Array[]): Uint8Array` — Returns a new `Uint8Array` whose contents are the concatenation of every `part` in order. Never throws. Alias for `concatUint8Arrays` from `@suaveplan/bytes`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
`concatBytes` is a direct `const` alias for `concatUint8Arrays` from `@suaveplan/bytes`. The underlying implementation uses two passes: the first sums `part.length` to pre-allocate once (no reallocation), the second uses `Uint8Array#set` which hits the fast path in every modern engine. Delegating here removes identical logic that previously lived in both packages and ensures all `@suaveplan/crypto-*` consumers share the same implementation with a single point of maintenance.
|
||||
|
||||
### Edge Cases
|
||||
|
||||
`concatBytes` preserves exact input order and is safe to call with zero, one, or many arrays. Passing a zero-length array returns a new zero-length `Uint8Array`. Callers relying on fixed output lengths for cryptographic framing should validate each input's length before calling; the function trusts its inputs and performs a single pre-sized allocation.
|
||||
@@ -0,0 +1,52 @@
|
||||
# i2osp — Integer-to-Octet-String Primitive
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
Encode a non-negative integer as a fixed-length big-endian byte string, per RFC 8017 §4.1 (PKCS #1 v2.2). Used by RSA-OAEP, RSA-PSS, ECDSA, and every hash-to-curve or MGF1 construction that has to pour a counter or length field into a digest input. Having a shared primitive means every consumer in the `@suaveplan/crypto-*` family encodes counters identically and rejects out-of-range inputs with the same stable error code — a concrete mitigation for the historical class of bugs where one module silently truncated a counter while another padded it.
|
||||
|
||||
## Features
|
||||
|
||||
- Accepts `number` or `bigint` input.
|
||||
- Big-endian output of exactly `xLen` bytes, zero-padded at the left.
|
||||
- Range-checks the output fits — never silently truncates.
|
||||
- Stable error codes: `CRYPTO.UTILS.INTEGER.OUT_OF_RANGE` (value negative or too large), `CRYPTO.UTILS.BYTES.INVALID_LENGTH` (`xLen` not a non-negative integer).
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { i2osp } from "@suaveplan/crypto-utils/bytes";
|
||||
|
||||
const counter = i2osp(0x01020304, 4); // Uint8Array([0x01, 0x02, 0x03, 0x04])
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { concatBytes, i2osp } from "@suaveplan/crypto-utils/bytes";
|
||||
import { sha256 } from "@suaveplan/crypto-hash/sha256";
|
||||
|
||||
// MGF1 (RFC 8017 §B.2.1)
|
||||
function mgf1(seed: Uint8Array, maskLen: number): Uint8Array {
|
||||
const T: Uint8Array[] = [];
|
||||
let accLen = 0;
|
||||
for (let c = 0; accLen < maskLen; c++) {
|
||||
const block = sha256(concatBytes(seed, i2osp(c, 4)));
|
||||
T.push(block);
|
||||
accLen += block.length;
|
||||
}
|
||||
return concatBytes(...T).slice(0, maskLen);
|
||||
}
|
||||
|
||||
const mask = mgf1(new Uint8Array(32), 48);
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `i2osp(n: number | bigint, xLen: number): Uint8Array` — Returns a `Uint8Array` of length `xLen` whose big-endian interpretation equals `n`. Throws `CryptoUtilsError` with code `CRYPTO.UTILS.INTEGER.OUT_OF_RANGE` if `n` is negative or does not fit in `xLen` bytes. Throws `CRYPTO.UTILS.BYTES.INVALID_LENGTH` if `xLen` is negative or non-integer.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
All arithmetic is routed through `BigInt` so `number` values up to `Number.MAX_SAFE_INTEGER` and larger `bigint` values are handled uniformly. The inner loop writes right-to-left (`i = xLen - 1` down to `0`), shifting by 8 bits each iteration. A final check (`v !== 0n`) detects overflow without needing a separate bit-length calculation. The partner primitive `os2ip` lives alongside and reverses the mapping.
|
||||
@@ -0,0 +1,51 @@
|
||||
# os2ip — Octet-String-to-Integer Primitive
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
The inverse of `i2osp`. Decode a big-endian byte string into a `bigint`, per RFC 8017 §4.2 (PKCS #1 v2.2). Returns `bigint` unconditionally so callers never lose precision, which matters for RSA moduli, ECDSA `r`/`s` pairs, and any Montgomery-ladder scalar.
|
||||
|
||||
## Features
|
||||
|
||||
- Always returns `bigint`; never lossy.
|
||||
- Big-endian decode.
|
||||
- Empty input → `0n` (matches RFC convention for a zero-length octet string representing 0).
|
||||
- Zero dependencies; pure TypeScript.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { os2ip } from "@suaveplan/crypto-utils/bytes";
|
||||
|
||||
const n = os2ip(new Uint8Array([0xde, 0xad, 0xbe, 0xef])); // 3_735_928_559n
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { i2osp, os2ip } from "@suaveplan/crypto-utils/bytes";
|
||||
|
||||
// Check that a candidate scalar is strictly less than the group order.
|
||||
function scalarIsInRange(scalar: Uint8Array, order: bigint): boolean {
|
||||
const n = os2ip(scalar);
|
||||
return n > 0n && n < order;
|
||||
}
|
||||
|
||||
const order = 0xfffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141n;
|
||||
const candidate = i2osp(1234567890n, 32);
|
||||
console.log(scalarIsInRange(candidate, order));
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `os2ip(bytes: Uint8Array): bigint` — Decodes `bytes` as a big-endian non-negative integer. Empty input yields `0n`. Never throws.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
A single-pass fold: `n = (n << 8) | byte[i]`. The shift-OR form avoids a multiply by 256, which `BigInt` engines internally implement as a shift anyway, but the explicit form is easier to audit. For inputs over a few kilobytes the `BigInt` arithmetic dominates; callers that decode large inputs hot should consider streaming parsing at the protocol layer instead of materialising a `BigInt`.
|
||||
|
||||
### RFC 8017 Context
|
||||
|
||||
`os2ip` (Octet-String-to-Integer Primitive) is defined by PKCS#1 v2.2 / RFC 8017 §4.2 and converts a big-endian octet string to a non-negative `bigint`. Together with its inverse `i2osp` it forms the byte-level glue between cryptographic framing layers and integer-arithmetic primitives such as RSA modular exponentiation. Leading zero bytes are preserved at the integer level only insofar as they do not affect magnitude — the caller must track octet-string length separately.
|
||||
48
docs/security-stack/crypto/crypto-utils/src/bytes/xor/xor.md
Normal file
48
docs/security-stack/crypto/crypto-utils/src/bytes/xor/xor.md
Normal file
@@ -0,0 +1,48 @@
|
||||
# xor
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
Byte-wise XOR of two equal-length `Uint8Array` buffers. XOR is the kernel of every stream cipher and every one-time-pad construction, and appears inside HMAC (ipad/opad), counter-mode encryption, and KDF mixing steps. A dedicated helper with a strict length check avoids two footguns that recur in crypto code: (1) silently truncating or extending one operand and (2) leaking a length-mismatch by differential timing in the caller's branch.
|
||||
|
||||
## Features
|
||||
|
||||
- Strict length check — throws on mismatch rather than truncating or extending.
|
||||
- Stable error code `CRYPTO.UTILS.BYTES.LENGTH_MISMATCH` with both lengths in context.
|
||||
- Returns a freshly-allocated output (never mutates inputs).
|
||||
- Pure TypeScript, tight inner loop.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { xor } from "@suaveplan/crypto-utils/bytes";
|
||||
|
||||
const stream = xor(keystream, plaintext); // ciphertext, if keystream is one-shot
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { xor } from "@suaveplan/crypto-utils/bytes";
|
||||
import { concatBytes } from "@suaveplan/crypto-utils/bytes";
|
||||
|
||||
// HMAC inner/outer pad construction: `ipad = key ⊕ 0x36…, opad = key ⊕ 0x5c…`
|
||||
function hmacPads(paddedKey: Uint8Array): { ipad: Uint8Array; opad: Uint8Array } {
|
||||
const b = paddedKey.length;
|
||||
const i36 = new Uint8Array(b).fill(0x36);
|
||||
const o5c = new Uint8Array(b).fill(0x5c);
|
||||
return { ipad: xor(paddedKey, i36), opad: xor(paddedKey, o5c) };
|
||||
}
|
||||
|
||||
const { ipad, opad } = hmacPads(new Uint8Array(64));
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `xor(a: Uint8Array, b: Uint8Array): Uint8Array` — returns a new `Uint8Array` whose elements are `a[i] ^ b[i]`. Throws `CryptoUtilsError` with code `CRYPTO.UTILS.BYTES.LENGTH_MISMATCH` and context `{ aLen, bLen }` when inputs differ in length.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The inner loop reads both operands by index and writes the output in one pass, which every modern engine turns into a single SIMD-friendly memory-store pattern. We do not expose an in-place `xorInto` variant because XOR on aliased buffers is a common source of correctness bugs in cipher implementations; callers that need it should write `out[i] = a[i] ^ b[i]` explicitly where the aliasing semantics are obvious from context. Error context carries both lengths so the caller can diagnose without logging the (possibly secret) inputs.
|
||||
@@ -0,0 +1,52 @@
|
||||
# constantTimeEqual
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
Byte-for-byte comparison of two `Uint8Array` buffers that takes time dependent only on the input length, not on where they first differ. Use it — and never the built-in `===`, `Array.prototype.every`, or `Buffer.equals` — for comparing authentication tags, MAC digests, password hashes, and any other value where an attacker might use wall-clock time to learn a secret byte-by-byte.
|
||||
|
||||
## Features
|
||||
|
||||
- No early return on mismatch within the byte loop.
|
||||
- Constant-time in buffer **contents**; length check is the only short-circuit (lengths of MACs/tags are public constants).
|
||||
- Zero allocations; pure loop over the inputs.
|
||||
- No dependencies.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { constantTimeEqual } from "@suaveplan/crypto-utils/constant-time";
|
||||
|
||||
const tagReceived = new Uint8Array(16);
|
||||
const tagExpected = new Uint8Array(16);
|
||||
if (!constantTimeEqual(tagReceived, tagExpected)) {
|
||||
throw new Error("tag mismatch");
|
||||
}
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { constantTimeEqual } from "@suaveplan/crypto-utils/constant-time";
|
||||
|
||||
// Verify a computed MAC against a header-supplied expected MAC in one step,
|
||||
// without ever comparing via `===`, `for-of`, or `Buffer.equals`.
|
||||
function verifyMessageAuth(
|
||||
macImpl: (msg: Uint8Array) => Uint8Array,
|
||||
message: Uint8Array,
|
||||
expected: Uint8Array
|
||||
): boolean {
|
||||
const computed = macImpl(message);
|
||||
return constantTimeEqual(computed, expected);
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `constantTimeEqual(a: Uint8Array, b: Uint8Array): boolean` — returns `true` iff both buffers have the same length **and** every byte matches. When lengths differ, returns `false` immediately (length is not secret).
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The loop computes `diff |= a[i] ^ b[i]`, accumulating a nonzero value iff any pair of bytes differs; the final check `diff === 0` is also constant time because it inspects a single integer. JavaScript engines aggressively inline this pattern, so the generated machine code is effectively identical to the hand-written one found in `@noble/hashes` and `libsodium`'s `sodium_memcmp`. The length short-circuit exists because returning `false` on length mismatch does not leak secret information — MAC and tag lengths are public protocol constants — and because continuing the loop with a shorter buffer would require masking that adds complexity with no security benefit.
|
||||
@@ -0,0 +1,60 @@
|
||||
# Base58 encoding (Bitcoin alphabet)
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
Encodes and decodes byte buffers using the Bitcoin Base58 alphabet, which omits the four visually ambiguous characters `0`, `O`, `I`, and `l`. Base58 is the encoding of choice for address formats where a human will copy the string by eye (Bitcoin addresses, Solana public keys, some Ethereum vanity formats) and where the absence of punctuation lets the value travel through URLs, QR codes, and plain text without escaping. Leading zero bytes are preserved as the `'1'` prefix character, matching the behaviour of Bitcoin Base58Check address payloads.
|
||||
|
||||
## Features
|
||||
|
||||
- Bitcoin alphabet: `123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz`.
|
||||
- `BigInt`-based conversion — no floating point, no length limit beyond JS heap size.
|
||||
- Preserves leading-zero bytes as `'1'` prefix characters.
|
||||
- Stable error code `CRYPTO.UTILS.ENCODING.INVALID_INPUT` on decode failure.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { fromBase58, toBase58 } from "@suaveplan/crypto-utils/encoding";
|
||||
|
||||
const addr = toBase58(new Uint8Array([0x00, 0xeb, 0x15, 0x23])); // "1NS..."-style prefix
|
||||
const back = fromBase58(addr);
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { fromBase58, toBase58 } from "@suaveplan/crypto-utils/encoding";
|
||||
import { CryptoUtilsError } from "@suaveplan/crypto-utils/errors";
|
||||
|
||||
function verifyAddressPayload(address: string, expectedLeadingZeros: number): Uint8Array {
|
||||
let bytes: Uint8Array;
|
||||
try {
|
||||
bytes = fromBase58(address);
|
||||
} catch (err) {
|
||||
if (err instanceof CryptoUtilsError) {
|
||||
throw new Error(`Not a Base58 address: ${address}`);
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
let zeros = 0;
|
||||
while (zeros < bytes.length && bytes[zeros] === 0) zeros++;
|
||||
if (zeros !== expectedLeadingZeros) {
|
||||
throw new Error(`Expected ${expectedLeadingZeros} leading zero bytes, got ${zeros}`);
|
||||
}
|
||||
return bytes;
|
||||
}
|
||||
|
||||
verifyAddressPayload("1NS17iag9jJgTHD1VXjvLCEnZuQ3rJDE9L", 1);
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `toBase58(bytes: Uint8Array): string` — Encodes into the Bitcoin alphabet. Empty input produces the empty string. Leading zero bytes become leading `'1'` characters, one per zero byte.
|
||||
- `fromBase58(s: string): Uint8Array` — Decodes into bytes. Leading `'1'` characters become leading zero bytes. Throws `CryptoUtilsError(CRYPTO.UTILS.ENCODING.INVALID_INPUT)` on any out-of-alphabet character, with `context.input` truncated to the first 32 characters of the offending string.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
Conversion goes through `BigInt` so arbitrary-length buffers are supported without losing precision. The encoder counts leading zero bytes up front, then converts the remaining bytes to a `BigInt`, repeatedly divides by 58 to produce characters from least significant to most, and finally prepends one `'1'` per leading zero byte. The decoder is the reverse; it pre-counts `'1'` prefix characters, then accumulates `n = n * 58 + value` into a `BigInt`, and finally emits bytes by repeated `n >>= 8`. Base58 is intentionally slower than Base64 — its purpose is human-readability, not throughput. Use Base64/Base64Url for wire formats.
|
||||
@@ -0,0 +1,56 @@
|
||||
# Base64 encoding
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
Standard Base64 (RFC 4648 §4) and URL-safe Base64 (§5) encoding and decoding for byte buffers. Standard Base64 is the default for JSON, JWT headers, and binary payloads in MIME. URL-safe Base64 substitutes `-` and `_` for `+` and `/` so the result can appear in URLs and file paths without percent-escaping. Standard Base64 (`toBase64` / `fromBase64`) is delegated entirely to `@suaveplan/bytes`; the URL-safe variants are implemented here because they use a crypto-specific `+→- / →_` substitution pattern not present in the bytes package.
|
||||
|
||||
## Features
|
||||
|
||||
- `toBase64` / `fromBase64`: RFC 4648 §4 with `=` padding — delegated to `@suaveplan/bytes`.
|
||||
- `toBase64Url` / `fromBase64Url`: RFC 4648 §5 without padding — implemented natively by post-processing the standard base64 output.
|
||||
- URL-safe decoder tolerates optional trailing `=` padding.
|
||||
- Throws `BytesError` (from `@suaveplan/bytes`) on malformed standard base64; same error type propagates through the URL-safe decoder.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { fromBase64, toBase64 } from "@suaveplan/crypto-utils/encoding";
|
||||
|
||||
const s = toBase64(new TextEncoder().encode("foobar")); // "Zm9vYmFy"
|
||||
const bytes = fromBase64(s);
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { fromBase64Url, toBase64Url } from "@suaveplan/crypto-utils/encoding";
|
||||
import { BytesError } from "@suaveplan/bytes";
|
||||
|
||||
function parseJwtSegment(segment: string): Uint8Array {
|
||||
try {
|
||||
return fromBase64Url(segment);
|
||||
} catch (err) {
|
||||
if (err instanceof BytesError) {
|
||||
throw new Error(`JWT segment not valid base64url: ${segment.slice(0, 8)}…`);
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
const encoded = toBase64Url(new Uint8Array([0xfb, 0xff, 0xbf]));
|
||||
parseJwtSegment(encoded);
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `toBase64(bytes: Uint8Array): string` — RFC 4648 §4 with `=` padding. Never throws. Alias for `bytesToBase64` from `@suaveplan/bytes`.
|
||||
- `fromBase64(s: string): Uint8Array` — RFC 4648 §4. Throws `BytesError(BYTES.ENCODE.INVALID_BASE64)` on invalid characters or bad length.
|
||||
- `toBase64Url(bytes: Uint8Array): string` — RFC 4648 §5 without padding. Calls `toBase64` then substitutes `+→-`, `/→_`, strips `=`.
|
||||
- `fromBase64Url(s: string): Uint8Array` — RFC 4648 §5. Restores `+` and `/`, adds padding, then delegates to `fromBase64`. Tolerates optional trailing `=` padding. Throws `BytesError` on malformed input.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
Standard base64 (`toBase64` / `fromBase64`) are direct `const` aliases for `bytesToBase64` / `base64ToBytes` from `@suaveplan/bytes`, eliminating duplicate encoding logic. URL-safe base64 is implemented as a post-processing step: `toBase64Url` calls `toBase64` then performs three `String.replace` calls to swap characters and strip padding. `fromBase64Url` reverses the substitution, computes missing padding (aligning to a multiple of 4), then delegates to `fromBase64`. This approach ensures the URL-safe round-trip shares exactly the same underlying byte semantics as the standard variant. Error type for invalid input is `BytesError` (from `@suaveplan/bytes`) rather than the legacy `CryptoUtilsError`.
|
||||
@@ -0,0 +1,55 @@
|
||||
# Hex encoding
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
Convert between `Uint8Array` byte buffers and lowercase hexadecimal strings. Hex is the canonical human-readable representation of cryptographic material (digests, public keys, nonces) because every byte maps to exactly two characters with no ambiguity or padding. This module re-exports `bytesToHex` and `hexToBytes` from `@suaveplan/bytes` under the crypto-friendly names `toHex` / `fromHex`, ensuring every `@suaveplan/crypto-*` package produces byte-for-byte identical output and rejects malformed input with a stable error code.
|
||||
|
||||
## Features
|
||||
|
||||
- `toHex(bytes)` — lowercase, no `0x` prefix; delegates to `@suaveplan/bytes`.
|
||||
- `fromHex(hex)` — accepts both upper and lower case; rejects odd length or non-hex characters.
|
||||
- Throws `BytesError(BYTES.ENCODE.INVALID_HEX)` on malformed input (from `@suaveplan/bytes`).
|
||||
- Zero implementation code in this module — pure delegation ensures a single source of truth.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { fromHex, toHex } from "@suaveplan/crypto-utils/encoding";
|
||||
|
||||
const digest = new Uint8Array([0xde, 0xad, 0xbe, 0xef]);
|
||||
const s = toHex(digest); // "deadbeef"
|
||||
const back = fromHex(s); // Uint8Array([0xde, 0xad, 0xbe, 0xef])
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { fromHex, toHex } from "@suaveplan/crypto-utils/encoding";
|
||||
import { BytesError } from "@suaveplan/bytes";
|
||||
|
||||
function parseDigest(input: string): Uint8Array {
|
||||
try {
|
||||
return fromHex(input.trim().replace(/^0x/, ""));
|
||||
} catch (err) {
|
||||
if (err instanceof BytesError) {
|
||||
throw new Error(`digest must be hex-encoded: ${input.slice(0, 32)}`);
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
const digest = parseDigest("0xDEADBEEF");
|
||||
console.log(toHex(digest));
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `toHex(bytes: Uint8Array): string` — Returns a lowercase hex string whose length is `2 * bytes.length`. Never throws. Alias for `bytesToHex` from `@suaveplan/bytes`.
|
||||
- `fromHex(hex: string): Uint8Array` — Parses upper or lower case. Throws `BytesError` with code `BYTES.ENCODE.INVALID_HEX` on odd-length or non-hex input. Alias for `hexToBytes` from `@suaveplan/bytes`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
This module is a thin delegation layer: `toHex` and `fromHex` are direct `const` aliases for `bytesToHex` and `hexToBytes` from `@suaveplan/bytes`. No encoding logic lives here — this removes duplication between the two packages and ensures all `@suaveplan/crypto-*` consumers share exactly the same implementation. Errors thrown on invalid input originate from `@suaveplan/bytes` as `BytesError` instances; callers should catch `BytesError` rather than the old `CryptoUtilsError`. The public function names (`toHex` / `fromHex`) are preserved so no downstream caller requires import changes.
|
||||
@@ -0,0 +1,96 @@
|
||||
# codes
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
Defines the stable, namespaced error-code registry for `@suaveplan/crypto-utils`. Every error thrown by this package carries a code from this registry, which lets callers type-narrow on `error.code` rather than relying on string matching against message text.
|
||||
|
||||
## Features
|
||||
|
||||
- Single `defineCodes` call produces a frozen, read-only map of codes under the `CRYPTO.UTILS` namespace
|
||||
- Codes are plain string literals (`"CRYPTO.UTILS.PRIMITIVE_NOT_IMPLEMENTED"`, etc.) — safe to log, serialize, and compare across process boundaries
|
||||
- `CryptoUtilsCode` union type keeps downstream `catch` blocks fully typed without any runtime cost
|
||||
- Covers the full error surface of the package: encoding validation, byte-length mismatches, integer-range checks, Merkle-tree errors, RNG limits, and the primitive-not-implemented sentinel
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { CRYPTO_UTILS_CODES, type CryptoUtilsCode } from "@suaveplan/crypto-utils/errors/codes";
|
||||
|
||||
function handleError(code: CryptoUtilsCode): string {
|
||||
switch (code) {
|
||||
case CRYPTO_UTILS_CODES.ENCODING_INVALID_INPUT:
|
||||
return "The supplied string is not valid for the chosen encoding.";
|
||||
case CRYPTO_UTILS_CODES.PRIMITIVE_NOT_IMPLEMENTED:
|
||||
return "This cryptographic primitive has no native implementation yet.";
|
||||
default:
|
||||
return "Unknown crypto-utils error.";
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
Type-narrowing in a catch block using the `CryptoUtilsCode` union:
|
||||
|
||||
```ts
|
||||
import { CRYPTO_UTILS_CODES, type CryptoUtilsCode } from "@suaveplan/crypto-utils/errors/codes";
|
||||
import { CryptoUtilsError } from "@suaveplan/crypto-utils/errors/crypto-utils-error";
|
||||
|
||||
async function safeHex(input: string): Promise<Uint8Array | null> {
|
||||
try {
|
||||
const { hexToBytes } = await import("@suaveplan/crypto-utils/encoding/hex/hex");
|
||||
return hexToBytes(input);
|
||||
} catch (err) {
|
||||
if (err instanceof CryptoUtilsError) {
|
||||
const code: CryptoUtilsCode = err.code;
|
||||
if (code === CRYPTO_UTILS_CODES.ENCODING_INVALID_INPUT) {
|
||||
console.warn("Bad hex input, returning null:", err.message);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `CRYPTO_UTILS_CODES`
|
||||
|
||||
```ts
|
||||
const CRYPTO_UTILS_CODES: {
|
||||
readonly PRIMITIVE_NOT_IMPLEMENTED: "CRYPTO.UTILS.PRIMITIVE_NOT_IMPLEMENTED";
|
||||
readonly ENCODING_INVALID_INPUT: "CRYPTO.UTILS.ENCODING.INVALID_INPUT";
|
||||
readonly BYTES_LENGTH_MISMATCH: "CRYPTO.UTILS.BYTES.LENGTH_MISMATCH";
|
||||
readonly BYTES_INVALID_LENGTH: "CRYPTO.UTILS.BYTES.INVALID_LENGTH";
|
||||
readonly INTEGER_OUT_OF_RANGE: "CRYPTO.UTILS.INTEGER.OUT_OF_RANGE";
|
||||
readonly MERKLE_EMPTY_LEAVES: "CRYPTO.UTILS.MERKLE.EMPTY_LEAVES";
|
||||
readonly MERKLE_INVALID_PROOF: "CRYPTO.UTILS.MERKLE.INVALID_PROOF";
|
||||
readonly RNG_INVALID_LENGTH: "CRYPTO.UTILS.RNG.INVALID_LENGTH";
|
||||
};
|
||||
```
|
||||
|
||||
Frozen registry produced by `defineCodes("CRYPTO.UTILS", [...])`. Dot-separated sub-namespaces (`ENCODING.INVALID_INPUT`, `BYTES.LENGTH_MISMATCH`, etc.) become underscore-keyed properties on the returned object.
|
||||
|
||||
**Returns** — A read-only record whose values are opaque string literals suitable for stable logging and wire serialization.
|
||||
|
||||
---
|
||||
|
||||
### `CryptoUtilsCode`
|
||||
|
||||
```ts
|
||||
type CryptoUtilsCode = (typeof CRYPTO_UTILS_CODES)[keyof typeof CRYPTO_UTILS_CODES];
|
||||
```
|
||||
|
||||
Union of every string literal in `CRYPTO_UTILS_CODES`. Use this as the `code` parameter type in `catch` blocks or error-handler signatures to stay fully typed without importing the entire error object.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
`defineCodes` (from `@suaveplan/error`) is the single source of truth for code formatting: it prepends the namespace, freezes the resulting object, and guarantees that each value is a unique string. This file owns no logic — it is a pure declaration layer.
|
||||
|
||||
The dot-notation in the raw array entries (`"ENCODING.INVALID_INPUT"`) is a human-readable convention for grouping related codes within the namespace; the resulting object key uses the underscore-separated form (`ENCODING_INVALID_INPUT`). Consumers should always reference `CRYPTO_UTILS_CODES.*` constants rather than the raw string literals so that renames are caught at compile time.
|
||||
|
||||
Adding a new code requires only appending a string to the `as const` array in this file and optionally adding a new subclass to the `errors/` directory. No numeric IDs, no registry migrations.
|
||||
@@ -0,0 +1,102 @@
|
||||
# crypto-utils-error
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
Defines `CryptoUtilsError`, the package-level base error class for `@suaveplan/crypto-utils`. All errors thrown by encoding, byte-manipulation, Merkle-tree, random, and primitive-registry code in this package extend this class, giving callers a single catch target with a fully typed `code` discriminator.
|
||||
|
||||
## Features
|
||||
|
||||
- Extends `SuaveplanError<CryptoUtilsCode>` from `@suaveplan/error`, inheriting cause-chain traversal, Zod-validated wire format, redaction, and OTel span-event adapter
|
||||
- Constrains the generic `code` parameter to the `CryptoUtilsCode` union so TypeScript rejects out-of-range codes at compile time
|
||||
- Accepts the full `SuaveplanErrorOptions` bag (cause, context, redact, i18n key) forwarded unchanged to the base class
|
||||
- Acts as a `instanceof` pivot: `err instanceof CryptoUtilsError` is sufficient to confirm the error originated in this package without inspecting the code string
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { CryptoUtilsError } from "@suaveplan/crypto-utils/errors/crypto-utils-error";
|
||||
import { CRYPTO_UTILS_CODES } from "@suaveplan/crypto-utils/errors/codes";
|
||||
|
||||
function assertHexLength(s: string): void {
|
||||
if (s.length % 2 !== 0) {
|
||||
throw new CryptoUtilsError(
|
||||
CRYPTO_UTILS_CODES.ENCODING_INVALID_INPUT,
|
||||
"error.crypto.utils.encoding.invalid-hex-length",
|
||||
{ context: { input: s.slice(0, 16) } }
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
Catching only `CryptoUtilsError` while re-throwing everything else, and extracting structured context for observability:
|
||||
|
||||
```ts
|
||||
import { CryptoUtilsError } from "@suaveplan/crypto-utils/errors/crypto-utils-error";
|
||||
import { CRYPTO_UTILS_CODES } from "@suaveplan/crypto-utils/errors/codes";
|
||||
import { hexToBytes } from "@suaveplan/crypto-utils/encoding/hex/hex";
|
||||
|
||||
async function parseHexSafe(raw: string): Promise<Uint8Array> {
|
||||
try {
|
||||
return hexToBytes(raw);
|
||||
} catch (err) {
|
||||
if (err instanceof CryptoUtilsError) {
|
||||
// Structured context is always available on the base class
|
||||
const ctx = err.context ?? {};
|
||||
console.error("CryptoUtilsError", {
|
||||
code: err.code,
|
||||
message: err.message,
|
||||
context: ctx,
|
||||
});
|
||||
|
||||
if (err.code === CRYPTO_UTILS_CODES.ENCODING_INVALID_INPUT) {
|
||||
// Return an empty buffer on invalid encoding rather than crashing
|
||||
return new Uint8Array(0);
|
||||
}
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `CryptoUtilsError`
|
||||
|
||||
```ts
|
||||
class CryptoUtilsError extends SuaveplanError<CryptoUtilsCode> {
|
||||
public constructor(
|
||||
code: CryptoUtilsCode,
|
||||
messageKey: string,
|
||||
options?: SuaveplanErrorOptions
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
**Parameters**
|
||||
|
||||
| Parameter | Type | Description |
|
||||
|--------------|------------------------|----------------------------------------------------------------------------------------------------|
|
||||
| `code` | `CryptoUtilsCode` | One of the stable string literals from `CRYPTO_UTILS_CODES`. |
|
||||
| `messageKey` | `string` | Dot-separated i18n key (e.g. `"error.crypto.utils.encoding.invalid-hex-length"`). |
|
||||
| `options` | `SuaveplanErrorOptions \| undefined` | Optional bag: `cause`, `context` (arbitrary record), `redact` flag, resolver override. |
|
||||
|
||||
**Throws** — Never throws itself; this is a constructor.
|
||||
|
||||
**Inherits** from `SuaveplanError<CryptoUtilsCode>`:
|
||||
|
||||
- `.code` — the `CryptoUtilsCode` literal
|
||||
- `.context` — arbitrary structured metadata attached at throw site
|
||||
- `.cause` — original error (for cause-chain traversal)
|
||||
- `.toJSON()` / `.toBinary()` — Zod-validated wire serialisation
|
||||
- `.toOtelEvent()` — OpenTelemetry span-event adapter
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The constructor body consists entirely of a `super(...)` forwarding call. The `biome-ignore` comment suppresses the "useless constructor" lint rule because the public constructor is intentionally widening the visibility of the protected base constructor — without it, internal subclasses in this package could not call `new CryptoUtilsError(...)` from outside the class hierarchy.
|
||||
|
||||
`CryptoUtilsError` is not abstract: lower-level utilities that do not warrant a dedicated subclass can instantiate it directly. Higher-level subclasses (`PrimitiveNotImplemented`) extend it to add semantically richer properties (e.g. `primitiveId`).
|
||||
@@ -0,0 +1,101 @@
|
||||
# primitive-not-implemented
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
Defines `PrimitiveNotImplemented`, the error thrown by any native-implementation stub inside `impl/<id>.native.ts` files that have not yet been written. It signals that the caller requested a specific cryptographic primitive by ID but no concrete runtime implementation exists for it, as distinct from a configuration or validation error.
|
||||
|
||||
## Features
|
||||
|
||||
- Extends `CryptoUtilsError` with stable code `CRYPTO.UTILS.PRIMITIVE_NOT_IMPLEMENTED`
|
||||
- Carries the primitive identifier both in the i18n message key and on the strongly-typed `primitiveId` property for programmatic inspection without string parsing
|
||||
- Merges caller-supplied `context` with `{ primitiveId }` so the id appears in structured logs and OTel span events regardless of i18n resolution
|
||||
- Accepts the full `SuaveplanErrorOptions` bag for cause-chaining, redaction, and context forwarding
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
Throwing from a native-implementation stub:
|
||||
|
||||
```ts
|
||||
import { PrimitiveNotImplemented } from "@suaveplan/crypto-utils/errors/primitive-not-implemented";
|
||||
|
||||
// Inside impl/argon2id.native.ts
|
||||
export function hashArgon2idNative(): Uint8Array {
|
||||
throw new PrimitiveNotImplemented("argon2id");
|
||||
}
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
Catching `PrimitiveNotImplemented` in a primitive selector to fall back to a noble implementation:
|
||||
|
||||
```ts
|
||||
import { PrimitiveNotImplemented } from "@suaveplan/crypto-utils/errors/primitive-not-implemented";
|
||||
import { hashArgon2idNative } from "./impl/argon2id.native.js";
|
||||
import { hashArgon2idNoble } from "./impl/argon2id.noble.js";
|
||||
import type { Argon2idOptions } from "./argon2id.types.js";
|
||||
|
||||
export async function hashArgon2id(
|
||||
password: Uint8Array,
|
||||
salt: Uint8Array,
|
||||
opts: Argon2idOptions
|
||||
): Promise<Uint8Array> {
|
||||
try {
|
||||
return hashArgon2idNative(password, salt, opts);
|
||||
} catch (err) {
|
||||
if (err instanceof PrimitiveNotImplemented && err.primitiveId === "argon2id") {
|
||||
// Native path not available; fall through to pure-JS noble implementation
|
||||
return hashArgon2idNoble(password, salt, opts);
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The `primitiveId` property enables the selector to distinguish "not implemented" (expected during development) from other errors (unexpected, must propagate).
|
||||
|
||||
## API Reference
|
||||
|
||||
### `PrimitiveNotImplemented`
|
||||
|
||||
```ts
|
||||
class PrimitiveNotImplemented extends CryptoUtilsError {
|
||||
public readonly primitiveId: string;
|
||||
|
||||
public constructor(
|
||||
primitiveId: string,
|
||||
options?: SuaveplanErrorOptions
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
**Parameters**
|
||||
|
||||
| Parameter | Type | Description |
|
||||
|---------------|----------------------------------|------------------------------------------------------------------------------------------------------|
|
||||
| `primitiveId` | `string` | Identifier of the primitive that lacks a native implementation (e.g. `"argon2id"`, `"chacha20"`). |
|
||||
| `options` | `SuaveplanErrorOptions \| undefined` | Optional bag: `cause`, `context` (merged with `{ primitiveId }`), `redact`, resolver override. |
|
||||
|
||||
**Properties**
|
||||
|
||||
| Property | Type | Description |
|
||||
|---------------|----------|-----------------------------------------------------|
|
||||
| `primitiveId` | `string` | The primitive ID passed to the constructor. Read-only. |
|
||||
| `code` | `"CRYPTO.UTILS.PRIMITIVE_NOT_IMPLEMENTED"` | Fixed stable code inherited via `CryptoUtilsError`. |
|
||||
|
||||
**Inherits** from `CryptoUtilsError` → `SuaveplanError<CryptoUtilsCode>`:
|
||||
|
||||
- `.context` — includes at minimum `{ primitiveId }`
|
||||
- `.cause` — for cause-chaining
|
||||
- `.toJSON()` / `.toBinary()` — wire serialisation
|
||||
- `.toOtelEvent()` — OpenTelemetry span-event adapter
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The `primitiveId` is attached to both `error.primitiveId` and `error.context.primitiveId`. The duplication is intentional: the property provides a typed accessor without object-spread indexing, while the context entry ensures it flows through any OTel or structured-logging pipeline that records `error.context` generically.
|
||||
|
||||
The i18n message key `"error.crypto.utils.primitive-not-implemented"` is intentionally generic — the `primitiveId` in context gives translators and log consumers the specific detail. This keeps the number of translation keys bounded even as new primitives are added.
|
||||
|
||||
This error is expected during development when the noble implementation is available but the native path has not been written. In production builds all native stubs should either be implemented or the selector should never route to the native path, so encountering this error in a production log indicates a misconfigured build.
|
||||
@@ -0,0 +1,70 @@
|
||||
# merkleProof
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
Produce and verify a Merkle inclusion proof for a specific leaf index. A proof is an ordered list of sibling hashes that, together with the leaf, reconstructs the root. Given the root is already published (on-chain, in a receipt, in an SCT), a peer can verify that a particular leaf was part of the committed set in `O(log n)` hashes, without having to see the whole tree. Paired with `merkleRoot` from the same directory, these two primitives cover the build-side and the verify-side of every Merkle commitment scheme in the stack.
|
||||
|
||||
## Features
|
||||
|
||||
- Proofs carry `{ sibling, side }` tuples so verification is direction-aware.
|
||||
- Same odd-count duplication rule as `merkleRoot` (Bitcoin convention).
|
||||
- `verifyMerkleProof` uses `constantTimeEqual` for the final root comparison.
|
||||
- Throws `CryptoUtilsError(CRYPTO.UTILS.MERKLE.INVALID_PROOF)` on out-of-range index.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { sha256 } from "@suaveplan/crypto-hash/sha256";
|
||||
import { merkleProof, merkleRoot, verifyMerkleProof } from "@suaveplan/crypto-utils/merkle";
|
||||
|
||||
const leaves = [new Uint8Array([1]), new Uint8Array([2]), new Uint8Array([3]), new Uint8Array([4])];
|
||||
const root = merkleRoot(leaves, sha256);
|
||||
const proof = merkleProof(leaves, 2, sha256); // proof for leaves[2]
|
||||
console.log(verifyMerkleProof(leaves[2], proof, root, sha256)); // true
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { sha256 } from "@suaveplan/crypto-hash/sha256";
|
||||
import { merkleProof, verifyMerkleProof } from "@suaveplan/crypto-utils/merkle";
|
||||
|
||||
// Serialise a proof as hex for wire transport: every step is "L:<hex>" or "R:<hex>".
|
||||
function serialiseProof(proof: ReturnType<typeof merkleProof>): string {
|
||||
return proof
|
||||
.map((s) => `${s.side === "left" ? "L" : "R"}:${[...s.sibling].map((b) => b.toString(16).padStart(2, "0")).join("")}`)
|
||||
.join(",");
|
||||
}
|
||||
|
||||
function parseProof(s: string): ReturnType<typeof merkleProof> {
|
||||
return s.split(",").map((tok) => {
|
||||
const [side, hex] = tok.split(":");
|
||||
const sibling = new Uint8Array((hex ?? "").length / 2);
|
||||
for (let i = 0; i < sibling.length; i++) sibling[i] = parseInt((hex ?? "").slice(i * 2, i * 2 + 2), 16);
|
||||
return { sibling, side: side === "L" ? ("left" as const) : ("right" as const) };
|
||||
});
|
||||
}
|
||||
|
||||
declare const leaves: Uint8Array[];
|
||||
declare const root: Uint8Array;
|
||||
const wire = serialiseProof(merkleProof(leaves, 1, sha256));
|
||||
verifyMerkleProof(leaves[1], parseProof(wire), root, sha256);
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `merkleProof(leaves: readonly Uint8Array[], index: number, hash: HashFn): MerkleProofStep[]`
|
||||
- Throws `CryptoUtilsError(CRYPTO.UTILS.MERKLE.EMPTY_LEAVES)` on empty leaves.
|
||||
- Throws `CryptoUtilsError(CRYPTO.UTILS.MERKLE.INVALID_PROOF)` on index out of range.
|
||||
- Returns empty array for single-leaf trees.
|
||||
- `verifyMerkleProof(leaf: Uint8Array, proof: readonly MerkleProofStep[], root: Uint8Array, hash: HashFn): boolean`
|
||||
- Returns `true` iff `proof` applied to `leaf` with `hash` reproduces `root`.
|
||||
- Uses `constantTimeEqual` for the final comparison.
|
||||
- `type MerkleProofStep = { sibling: Uint8Array; side: 'left' | 'right' }`
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The proof walker tracks `idx` at each level and halves it on the way up (`idx = Math.floor(idx / 2)`). When `idx` is the last element of an odd-count level, its sibling is itself (duplication rule), producing a `{ side: 'right', sibling: <copy of self> }` step; verifying recomputes `hash(self || self)` and the root matches. The `side` field is chosen from the leaf's perspective, not the sibling's — a leaf that is the _right_ child has a `left` sibling, hence `side === 'left'` means the sibling is prepended. `verifyMerkleProof` uses `constantTimeEqual` on the final root comparison so proof checks in low-level verifier code do not leak timing information about which specific byte differed.
|
||||
@@ -0,0 +1,59 @@
|
||||
# merkleRoot
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
Compute the Merkle tree root over a list of leaf byte buffers using a caller-supplied hash function. Merkle trees are the canonical structure for compact commitment to a set of values (transaction inclusion in Bitcoin blocks, file manifests in IPFS, SCT inclusion in RFC 6962 Certificate Transparency, OTel log-inclusion proofs). Factoring the tree construction out into this package means downstream consumers never re-derive the "duplicate the last node at odd-count levels" corner case where Bitcoin has historically had CVEs.
|
||||
|
||||
## Features
|
||||
|
||||
- Generic over any hash function `(Uint8Array) => Uint8Array`.
|
||||
- Bitcoin convention on odd-count levels: the last element is duplicated.
|
||||
- RFC 6962 internal-node construction: `hash(left || right)` with no prefix byte (callers that need domain separation should pre-prefix their leaves or wrap the hash).
|
||||
- Non-mutating: leaves are copied internally.
|
||||
- Throws `CryptoUtilsError(CRYPTO.UTILS.MERKLE.EMPTY_LEAVES)` on empty input.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { sha256 } from "@suaveplan/crypto-hash/sha256";
|
||||
import { merkleRoot } from "@suaveplan/crypto-utils/merkle";
|
||||
|
||||
const leaves = [new Uint8Array([1]), new Uint8Array([2]), new Uint8Array([3])];
|
||||
const root = merkleRoot(leaves, sha256);
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { sha256 } from "@suaveplan/crypto-hash/sha256";
|
||||
import { concatBytes } from "@suaveplan/crypto-utils/bytes";
|
||||
import { merkleRoot } from "@suaveplan/crypto-utils/merkle";
|
||||
|
||||
// RFC 6962-style CT leaves: prefix 0x00 for leaf, 0x01 for internal — wrap the hash.
|
||||
const TAG_LEAF = new Uint8Array([0x00]);
|
||||
const TAG_NODE = new Uint8Array([0x01]);
|
||||
|
||||
function rfc6962Leaves(leaves: readonly Uint8Array[]): readonly Uint8Array[] {
|
||||
return leaves.map((l) => sha256(concatBytes(TAG_LEAF, l)));
|
||||
}
|
||||
function taggedHash(input: Uint8Array): Uint8Array {
|
||||
return sha256(concatBytes(TAG_NODE, input));
|
||||
}
|
||||
|
||||
const tlsExtensionRoot = merkleRoot(rfc6962Leaves(leaves), taggedHash);
|
||||
declare const leaves: Uint8Array[];
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `merkleRoot(leaves: readonly Uint8Array[], hash: (input: Uint8Array) => Uint8Array): Uint8Array`
|
||||
- Throws `CryptoUtilsError(CRYPTO.UTILS.MERKLE.EMPTY_LEAVES)` on an empty leaf list.
|
||||
- Returns the root bytes as produced by `hash`. For single-leaf trees the leaf itself is returned unchanged.
|
||||
- `type HashFn = (input: Uint8Array) => Uint8Array`
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The algorithm iterates level by level, pairing neighbours and applying `hash(concat(left, right))`. When a level has an odd number of entries, the last entry is duplicated (`level[i + 1] ?? level[i]`), which is the Bitcoin convention; RFC 6962 specifies a different handling (propagating the odd node up the tree), so CT implementations should wrap their leaves and pass tagged hashes rather than rely on this primitive's default. A partner primitive `merkleProof` lives alongside and produces an inclusion proof for a specific leaf index using the same layout.
|
||||
@@ -0,0 +1,67 @@
|
||||
# primitive-id
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
A single, stable registry of every cryptographic primitive id across the `@suaveplan/crypto-*` family. Codegen scripts, CI checks, the replaceability selector layer, and the `PrimitiveNotImplemented` error all read from this file so that a newly-added primitive lights up everywhere consistently, and a renamed or removed primitive triggers a type error in every consumer. Think of it as the `Rosetta Stone` of the crypto family.
|
||||
|
||||
## Features
|
||||
|
||||
- Per-category `as const` tuples (`HASH_PRIMITIVE_IDS`, `CURVE_PRIMITIVE_IDS`, `CIPHER_PRIMITIVE_IDS`) and a combined `PRIMITIVE_IDS` tuple.
|
||||
- Literal-typed union types (`HashPrimitiveId`, `CurvePrimitiveId`, `CipherPrimitiveId`, `PrimitiveId`).
|
||||
- `primitiveCategory(id)` classifier (`'hash' | 'curve' | 'cipher' | undefined`).
|
||||
- `isPrimitiveId(id)` type guard.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { isPrimitiveId, PRIMITIVE_IDS } from "@suaveplan/crypto-utils/primitive-id";
|
||||
|
||||
for (const id of PRIMITIVE_IDS) {
|
||||
console.log("registered:", id);
|
||||
}
|
||||
|
||||
console.log(isPrimitiveId("sha256")); // true
|
||||
console.log(isPrimitiveId("md5")); // false
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import {
|
||||
type PrimitiveId,
|
||||
primitiveCategory,
|
||||
} from "@suaveplan/crypto-utils/primitive-id";
|
||||
|
||||
function pathFor(id: PrimitiveId): string {
|
||||
const cat = primitiveCategory(id);
|
||||
switch (cat) {
|
||||
case "hash":
|
||||
return `@suaveplan/crypto-hash/src/primitives/${id}`;
|
||||
case "curve":
|
||||
return `@suaveplan/crypto-curve/src/primitives/${id}`;
|
||||
case "cipher":
|
||||
return `@suaveplan/crypto-cipher/src/primitives/${id}`;
|
||||
default:
|
||||
throw new Error(`unclassified id: ${id satisfies never}`);
|
||||
}
|
||||
}
|
||||
|
||||
console.log(pathFor("ed25519"));
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `HASH_PRIMITIVE_IDS: readonly ["sha256","sha384","sha512","hmac","hkdf","pbkdf2","argon2id"]`
|
||||
- `CURVE_PRIMITIVE_IDS: readonly ["secp256k1","ed25519","x25519","p256"]`
|
||||
- `CIPHER_PRIMITIVE_IDS: readonly ["aes-256-gcm","chacha20-poly1305","xchacha20-poly1305"]`
|
||||
- `PRIMITIVE_IDS: readonly [...HASH, ...CURVE, ...CIPHER]`
|
||||
- `type HashPrimitiveId`, `type CurvePrimitiveId`, `type CipherPrimitiveId`, `type PrimitiveId`
|
||||
- `primitiveCategory(id: string): 'hash' | 'curve' | 'cipher' | undefined`
|
||||
- `isPrimitiveId(id: string): id is PrimitiveId`
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The tuples are declared `as const` so they are both values (iterable at runtime, consumed by codegen) and literal types (`(typeof HASH_PRIMITIVE_IDS)[number]`). The classifier uses `Array#includes` cast through `readonly string[]` to avoid TypeScript's narrowing behaviour rejecting unknown string inputs — a narrower API (overloaded on `PrimitiveId`) was tried and rejected because the callers genuinely take arbitrary strings (from config files) and need the `undefined` case. Adding a new primitive here triggers a build-break everywhere the registry is exhaustively cased, which is exactly what the replaceability architecture wants.
|
||||
@@ -0,0 +1,57 @@
|
||||
# randomBytes
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
Produce cryptographically-secure random bytes. Every `@suaveplan/crypto-*` package routes entropy through this single helper so there is exactly one callsite of `globalThis.crypto.getRandomValues` to audit, and callers that need deterministic entropy in tests (or a hardware-RNG adapter in production) can swap it with dependency injection rather than monkey-patching `globalThis`.
|
||||
|
||||
## Features
|
||||
|
||||
- Wraps `globalThis.crypto.getRandomValues` (Web Crypto) — works in Node 20+, Bun, browsers, Deno, Cloudflare Workers.
|
||||
- Dependency-injectable `ByteSource` for tests and hardware RNG adapters.
|
||||
- Explicit `n` range check; throws stable `CRYPTO.UTILS.RNG.INVALID_LENGTH` on bad input.
|
||||
- Returns a fresh `Uint8Array` every call.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { randomBytes } from "@suaveplan/crypto-utils/random";
|
||||
|
||||
const nonce = randomBytes(12);
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { randomBytes, type ByteSource } from "@suaveplan/crypto-utils/random";
|
||||
|
||||
// Deterministic source for tests — feeds a known sequence.
|
||||
function fixedSequence(bytes: Uint8Array): ByteSource {
|
||||
let pos = 0;
|
||||
return (n) => {
|
||||
const out = bytes.slice(pos, pos + n);
|
||||
pos += n;
|
||||
return out;
|
||||
};
|
||||
}
|
||||
|
||||
function generateKey(opts?: { byteSource?: ByteSource }): Uint8Array {
|
||||
return randomBytes(32, opts);
|
||||
}
|
||||
|
||||
const testKey = generateKey({
|
||||
byteSource: fixedSequence(new Uint8Array(32).fill(0xaa)),
|
||||
});
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `randomBytes(n: number, options?: { byteSource?: ByteSource }): Uint8Array` — returns `n` random bytes. Throws `CryptoUtilsError(CRYPTO.UTILS.RNG.INVALID_LENGTH)` if `n` is not a non-negative integer.
|
||||
- `defaultByteSource: ByteSource` — the `getRandomValues`-backed source. Exported for adapters that want to delegate to it.
|
||||
- `type ByteSource = (n: number) => Uint8Array` — the injection surface.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
`globalThis.crypto` is a WHATWG Web Crypto `Crypto` instance on every supported runtime. In Node we rely on the polyfill injected at test-setup time (`node:crypto` `webcrypto`). The test-only `byteSource` option is deliberately on the call, not a module-level setter, so there is never a case where a test leaks a deterministic source into production code. The length check rejects `NaN`, non-integers, and negatives — all would previously have been forwarded to `getRandomValues`, which throws a less-precise `RangeError`.
|
||||
@@ -0,0 +1,68 @@
|
||||
# diffFuzz — differential-fuzz harness
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
Run N iterations of random input against two implementations of the same primitive and report the first diverging reproducer, plus the total number of mismatches. Designed specifically for the "noble vs native" gating in `crypto-primitive-replaceability` Phase 1+: before a native implementation is allowed to flip to `'native'` in `crypto.config.ts`, it must run through `diffFuzz` against the reference `noble` impl without a single mismatch across millions of random inputs.
|
||||
|
||||
## Features
|
||||
|
||||
- Deterministic `byteSource` injection for reproducible fuzz campaigns.
|
||||
- Generic over input and output types — works for hashes, ciphers, MACs, curve ops.
|
||||
- Captures first reproducer with the exact input, both outputs, and iteration index for CI log forensics.
|
||||
- Continues running after the first mismatch so the `mismatches` count approximates the rate, not just the existence of a bug.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { diffFuzz } from "@suaveplan/crypto-utils/testing";
|
||||
|
||||
const result = diffFuzz({
|
||||
iterations: 1000,
|
||||
sample: (src) => src(32),
|
||||
implA: (buf: Uint8Array) => buf.reduce((a, b) => a + b, 0),
|
||||
implB: (buf: Uint8Array) => buf.reduce((a, b) => a + b, 0),
|
||||
equals: (a, b) => a === b,
|
||||
});
|
||||
console.log(result.mismatches); // 0
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { diffFuzz } from "@suaveplan/crypto-utils/testing";
|
||||
import { constantTimeEqual } from "@suaveplan/crypto-utils/constant-time";
|
||||
|
||||
// Differential fuzz SHA-256 noble vs native
|
||||
declare function sha256Noble(msg: Uint8Array): Uint8Array;
|
||||
declare function sha256Native(msg: Uint8Array): Uint8Array;
|
||||
|
||||
const result = diffFuzz({
|
||||
iterations: 100_000,
|
||||
sample: (src, iter) => src(iter % 4096), // variable-length messages
|
||||
implA: sha256Noble,
|
||||
implB: sha256Native,
|
||||
equals: constantTimeEqual,
|
||||
});
|
||||
|
||||
if (result.firstMismatch !== undefined) {
|
||||
const { iter, input, outputA, outputB } = result.firstMismatch;
|
||||
throw new Error(`SHA-256 diff at iter=${iter} len=${input.length} noble=${outputA.length}B native=${outputB.length}B`);
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `diffFuzz<V, E>(opts: DiffFuzzOptions<V, E>): DiffFuzzResult<V, E>`
|
||||
- `opts.iterations: number` — how many trials to run.
|
||||
- `opts.sample: (byteSource, iter) => V` — builds a fresh input each iteration. Receives the iteration index so callers can vary input shape by iteration.
|
||||
- `opts.implA`, `opts.implB: (input: V) => E`
|
||||
- `opts.equals: (a: E, b: E) => boolean`
|
||||
- `opts.byteSource?: ByteSource` — optional deterministic source; defaults to `defaultByteSource`.
|
||||
- Returns `{ iterations, mismatches, firstMismatch? }`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The loop is intentionally unrolled to a single pair of calls per iteration — no async, no microtasks, no chunking. For 100 000 iterations of SHA-256 this completes in a few hundred milliseconds in Node 20+. Callers that need structured output (e.g. `DifferentialFuzzCorpus` for CI regression pinning) should compose on top of this primitive rather than complicate its API. The `byteSource` injection is how reproducibility is achieved: when a CI job reports a fail, the caller can replay the run with a fixed source seeded from the failing iteration's context.
|
||||
@@ -0,0 +1,118 @@
|
||||
# dudect
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
`dudect` is a minimal constant-time smoke check for secret-handling code.
|
||||
It runs a function many times against two input classes — a fixed / low-entropy
|
||||
class and a random / high-entropy class — and applies Welch's t-test to the
|
||||
per-call latencies. A large absolute t-statistic suggests the function's
|
||||
runtime depends on its input, i.e. a potential timing side channel.
|
||||
|
||||
The harness is a statistical signal, not cryptographic analysis. It catches
|
||||
the obvious class of leaks (secret-indexed table lookups, early returns on
|
||||
secret byte comparisons) on hardware where the noise floor is low enough. It
|
||||
does NOT prove constant-time behaviour in a formal sense; production primitives
|
||||
still require code review and, ideally, formally-verified implementations.
|
||||
|
||||
The `crypto:timing` turbo task wires this harness up to run nightly against
|
||||
any `impl/*.native.ts` in `crypto-hash`, `crypto-curve`, or `crypto-cipher`
|
||||
that isn't still a `PrimitiveNotImplemented` stub. In Phase 0 every native
|
||||
is a stub, so the nightly runs a no-op and exits clean — the wiring is live
|
||||
and fires the moment a real native lands.
|
||||
|
||||
## Features
|
||||
|
||||
- Welch's t-test on interleaved class-A / class-B latency samples
|
||||
- Configurable total measurement count, warmup, and t-threshold
|
||||
- Deterministic control-flow — no I/O, no network, no filesystem
|
||||
- Returns numeric result (mean, variance, t-statistic) plus boolean verdict
|
||||
- Reference implementation of the test from Reparaz et al., DATE 2017
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { dudect } from "@suaveplan/crypto-utils/testing/dudect/dudect";
|
||||
|
||||
const result = dudect<Uint8Array>({
|
||||
fn: (buf) => nativeSha256(buf),
|
||||
sampleFixed: () => new Uint8Array(64),
|
||||
sampleRandom: () => crypto.getRandomValues(new Uint8Array(64)),
|
||||
});
|
||||
|
||||
if (!result.constantTime) {
|
||||
throw new Error(`sha256 native leaked timing: t=${result.tStatistic}`);
|
||||
}
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { dudect } from "@suaveplan/crypto-utils/testing/dudect/dudect";
|
||||
import { hmac as nativeHmac } from "@suaveplan/crypto-hash/primitives/hmac/impl/hmac.native";
|
||||
|
||||
const key = new Uint8Array(32);
|
||||
|
||||
const result = dudect<Uint8Array>({
|
||||
fn: (msg) => nativeHmac("SHA-256", key, msg),
|
||||
sampleFixed: () => new Uint8Array(128), // all zeros
|
||||
sampleRandom: () => {
|
||||
const buf = new Uint8Array(128);
|
||||
crypto.getRandomValues(buf);
|
||||
return buf;
|
||||
},
|
||||
measurements: 100_000,
|
||||
warmup: 5_000,
|
||||
tThreshold: 4.5,
|
||||
});
|
||||
|
||||
console.log({
|
||||
meanFixed: result.meanA,
|
||||
meanRandom: result.meanB,
|
||||
t: result.tStatistic,
|
||||
verdict: result.constantTime ? "PASS" : "LEAK",
|
||||
});
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `dudect<V>(opts: DudectOptions<V>): DudectResult`
|
||||
|
||||
Run the dudect t-test.
|
||||
|
||||
**Parameters**
|
||||
|
||||
- `opts.fn` — function under test. Must do real work; a short-circuiting
|
||||
`fn` produces meaningless statistics.
|
||||
- `opts.sampleFixed` — returns a class-A (fixed / low-entropy) input.
|
||||
- `opts.sampleRandom` — returns a class-B (random / high-entropy) input of
|
||||
the same shape as class A.
|
||||
- `opts.measurements` — total timed calls, split evenly between the two
|
||||
classes. Default `20_000`.
|
||||
- `opts.warmup` — pre-measurement iterations discarded. Default `1_000`.
|
||||
- `opts.tThreshold` — absolute-value threshold for the leak verdict.
|
||||
Default `4.5` (per Reparaz et al.).
|
||||
|
||||
**Returns** `DudectResult` with per-class counts, means, variances, the
|
||||
Welch t-statistic, the configured threshold, and a boolean `constantTime`
|
||||
verdict (`true` when `|t| < threshold`).
|
||||
|
||||
**Throws** never; degenerate inputs (zero variance in both classes) yield
|
||||
`tStatistic === 0` which registers as constant-time.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- `performance.now()` provides sub-ms resolution under Bun and modern Node.
|
||||
Precision degrades in sandboxed environments; interpret tight verdicts
|
||||
under VM / container load with care.
|
||||
- Class A and class B measurements are interleaved one-for-one. That smooths
|
||||
out GC pauses and clock drift that would otherwise bias the comparison.
|
||||
- Welch's t-test is the right tool because class-A and class-B variances
|
||||
are rarely equal in JavaScript — we cannot rely on Student's t.
|
||||
- The harness is intentionally small (~130 lines). It is not a replacement
|
||||
for formal constant-time analysis or `valgrind --track-origins` on a
|
||||
native binary; it is a cheap continuous-integration signal.
|
||||
- Hardware variability matters. CI-runner results should be interpreted
|
||||
relative to a historical baseline, not as an absolute pass/fail.
|
||||
@@ -0,0 +1,70 @@
|
||||
# runVectors — KAT harness
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
Generic Known-Answer-Test runner. Given a list of `TestVector`s (input + expected output), a list of `Impl`s (functions that map input → output), and an equality predicate, it executes the cross product and returns structured pass / fail / pending counts per implementation. Every `impl/<id>.noble.ts` and `impl/<id>.native.ts` pair across the `@suaveplan/crypto-*` family drives its own test suite through this harness so the pass/pending/fail matrix is reported uniformly.
|
||||
|
||||
## Features
|
||||
|
||||
- Generic over input and output types.
|
||||
- Catches `PrimitiveNotImplemented` and marks those vectors "pending" so unimplemented native tiers don't colour red.
|
||||
- Per-impl summary: `pass`, `fail`, `pending` counts.
|
||||
- Preserves vector order in the per-impl result list so failing vectors can be located by name.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { runVectors } from "@suaveplan/crypto-utils/testing";
|
||||
|
||||
const report = runVectors({
|
||||
vectors: [{ name: "v1", input: 2, expected: 4 }],
|
||||
impls: [{ name: "square", run: (n: number) => n * n }],
|
||||
equals: (a, b) => a === b,
|
||||
});
|
||||
console.log(report[0].summary); // { pass: 1, fail: 0, pending: 0 }
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { runVectors } from "@suaveplan/crypto-utils/testing";
|
||||
import { constantTimeEqual } from "@suaveplan/crypto-utils/constant-time";
|
||||
import { fromHex } from "@suaveplan/crypto-utils/encoding";
|
||||
import { PrimitiveNotImplemented } from "@suaveplan/crypto-utils/errors";
|
||||
|
||||
type V = { msg: Uint8Array };
|
||||
type E = Uint8Array;
|
||||
|
||||
const vectors = [
|
||||
{ name: "empty", input: { msg: new Uint8Array() }, expected: fromHex("e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855") },
|
||||
];
|
||||
|
||||
const report = runVectors<V, E>({
|
||||
vectors,
|
||||
impls: [
|
||||
{ name: "noble", run: ({ msg }) => sha256Noble(msg) },
|
||||
{ name: "native", run: () => { throw new PrimitiveNotImplemented("sha256"); } },
|
||||
],
|
||||
equals: constantTimeEqual,
|
||||
});
|
||||
|
||||
declare function sha256Noble(m: Uint8Array): Uint8Array;
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `runVectors<V, E>(opts: RunVectorsOptions<V, E>): readonly ImplReport[]`
|
||||
- `opts.vectors: readonly TestVector<V, E>[]`
|
||||
- `opts.impls: readonly Impl<V, E>[]`
|
||||
- `opts.equals: (a: E, b: E) => boolean` — for byte outputs, pass `constantTimeEqual`; for structural outputs, pass a deep-equal.
|
||||
- `type TestVector<V, E> = { name: string; input: V; expected: E }`
|
||||
- `type Impl<V, E> = { name: string; run: (input: V) => E }`
|
||||
- `type VectorResult = { name: string; status: 'pass' | 'fail' | 'pending'; reason?: string }`
|
||||
- `type ImplReport = { impl: string; results: readonly VectorResult[]; summary: { pass; fail; pending } }`
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The harness detects `PrimitiveNotImplemented` by checking `err.constructor.name === 'PrimitiveNotImplemented'` rather than `instanceof`. This decouples the harness from the error class's module identity (which would fail across bundler boundaries and `vite-node` hot reloads) and matches the convention used by `@noble/hashes`' own test helpers. Any other exception is captured as `fail` with its `message` as the `reason`. The harness is intentionally synchronous — KAT vectors are always fast — which keeps Vitest reporting tight.
|
||||
@@ -0,0 +1,61 @@
|
||||
# zeroize
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
Best-effort overwrite of a `Uint8Array` with zero bytes. Secret material — private keys, AEAD keys, plaintext buffers, intermediate KDF state — should be zeroized as soon as it is no longer needed so that its remnants become unreadable if the process is later snapshotted, dumped, or forked into an attacker-controlled context.
|
||||
|
||||
JavaScript does not let us pin memory or guarantee the garbage collector has not already copied the underlying `ArrayBuffer` into a new region. `zeroize` is therefore a mitigation, not a guarantee. Use it in combination with narrow scoping (allocate the buffer inside a function, zeroize, return nothing sensitive) to minimise the residency window.
|
||||
|
||||
## Features
|
||||
|
||||
- In-place; no allocation.
|
||||
- Backed by `Uint8Array#fill(0)`, which is inlined to a tight memset on every modern engine.
|
||||
- Zero dependencies.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { zeroize } from "@suaveplan/crypto-utils/zeroize";
|
||||
|
||||
const key = new Uint8Array(32);
|
||||
// …use the key…
|
||||
zeroize(key);
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { zeroize } from "@suaveplan/crypto-utils/zeroize";
|
||||
import { concatBytes } from "@suaveplan/crypto-utils/bytes";
|
||||
|
||||
function withScopedSecret<T>(
|
||||
secretBuilder: () => Uint8Array,
|
||||
fn: (secret: Uint8Array) => T
|
||||
): T {
|
||||
const secret = secretBuilder();
|
||||
try {
|
||||
return fn(secret);
|
||||
} finally {
|
||||
zeroize(secret);
|
||||
}
|
||||
}
|
||||
|
||||
withScopedSecret(
|
||||
() => concatBytes(new Uint8Array(16).fill(0xaa), new Uint8Array(16).fill(0xbb)),
|
||||
(key) => {
|
||||
// key is live inside this block only
|
||||
return key.length;
|
||||
}
|
||||
);
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `zeroize(buf: Uint8Array): void` — fills `buf` with `0` in place. Returns nothing. Never throws.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
`Uint8Array#fill(0)` is the minimum the runtime can do without dropping into `node:buffer` or platform-specific APIs. Some environments (OpenSSL-linked `node:crypto`) expose `sodium_memzero`-equivalents that are marked volatile to prevent compiler dead-store elimination; we do not rely on those here because they are not available in browsers or edge runtimes, and cross-runtime consistency is more valuable than the marginal hardening on Node. Engines do not currently eliminate `Uint8Array#fill` as dead, but a future JIT optimisation could; revisit this helper if `dudect`-style analysis reports leakage.
|
||||
Reference in New Issue
Block a user