Move docs/ package mirror into document-navigator plugin
Relocate the ~6k-file docs tree from the repo root into plugins/document-navigator/docs/ so the plugin is self-contained and portable. docs/superpowers/ (specs and plans) stays at the repo root. Moved via cp+rm rather than git mv because directory rename() fails with EPERM on this WSL /mnt DrvFs mount; git rename-detection pairs the identical-content files at commit time. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,7 @@
|
||||
# @suaveplan/crypto-hash Changelog
|
||||
|
||||
## 0.0.0 — suaveplan
|
||||
|
||||
Initial suaveplan implementation. Ships SHA-2 digests, HMAC, HKDF, PBKDF2,
|
||||
Argon2id, hex encoding, and Base64/Base64Url encoding. All functions are
|
||||
pure TypeScript with zero WASM and zero browser globals.
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,125 @@
|
||||
# base64
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
Provides pure-TypeScript Base64 encoding and decoding in both standard (RFC
|
||||
4648 §4) and URL-safe (RFC 4648 §5) variants. Cryptographic output such as
|
||||
JWT payloads, bearer tokens, and encoded public keys is routinely transmitted
|
||||
as Base64Url, while storage formats and MIME payloads use standard Base64.
|
||||
This module implements both without depending on Node.js `Buffer` or `btoa`/
|
||||
`atob` (which are absent in some edge runtimes), making it safe for all
|
||||
`@suaveplan/universal` consumers.
|
||||
|
||||
## Features
|
||||
|
||||
- `toBase64(bytes)` — standard Base64 with `=` padding
|
||||
- `fromBase64(b64)` — decode standard Base64; throws `HashError` on malformed input
|
||||
- `toBase64Url(bytes)` — URL-safe Base64, no padding, `-` and `_` alphabet
|
||||
- `fromBase64Url(b64url)` — decode URL-safe Base64; throws `HashError` on malformed input
|
||||
- Passes RFC 4648 test vectors
|
||||
- Pure TypeScript — no `Buffer`, no `btoa`/`atob`, no Node built-ins
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { toBase64, fromBase64 } from "@suaveplan/crypto-hash/encoding";
|
||||
|
||||
const encoder = new TextEncoder();
|
||||
const bytes = encoder.encode("hello world");
|
||||
|
||||
const b64 = toBase64(bytes);
|
||||
console.log(b64); // "aGVsbG8gd29ybGQ="
|
||||
|
||||
const decoded = fromBase64(b64);
|
||||
console.log(new TextDecoder().decode(decoded)); // "hello world"
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { toBase64Url, fromBase64Url } from "@suaveplan/crypto-hash/encoding";
|
||||
import { sha256 } from "@suaveplan/crypto-hash/hash";
|
||||
import { HashError } from "@suaveplan/crypto-hash/errors";
|
||||
|
||||
// Build a simple JWT-style header.payload structure
|
||||
function buildToken(payload: object, sigKey: Uint8Array): string {
|
||||
const encoder = new TextEncoder();
|
||||
const header = toBase64Url(encoder.encode(JSON.stringify({ alg: "HS256" })));
|
||||
const body = toBase64Url(encoder.encode(JSON.stringify(payload)));
|
||||
return `${header}.${body}`;
|
||||
}
|
||||
|
||||
// Parse and validate a Base64Url segment
|
||||
function decodeSegment(segment: string): object {
|
||||
try {
|
||||
const bytes = fromBase64Url(segment);
|
||||
return JSON.parse(new TextDecoder().decode(bytes)) as object;
|
||||
} catch (err) {
|
||||
if (err instanceof HashError) {
|
||||
throw new TypeError(`Malformed Base64Url segment: ${err.code}`);
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
const token = buildToken({ sub: "user-123", iat: 1700000000 }, new Uint8Array(32));
|
||||
console.log(token);
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `toBase64(bytes: Uint8Array): string`
|
||||
|
||||
Encodes `bytes` as standard Base64 with `=` padding.
|
||||
|
||||
| Parameter | Type | Description |
|
||||
|-----------|------|-------------|
|
||||
| `bytes` | `Uint8Array` | Byte sequence to encode |
|
||||
|
||||
**Returns:** Base64 string of length `Math.ceil(bytes.length / 3) * 4`.
|
||||
|
||||
---
|
||||
|
||||
### `fromBase64(b64: string): Uint8Array`
|
||||
|
||||
Decodes a standard Base64 string. Padding (`=`) is optional.
|
||||
|
||||
| Parameter | Type | Description |
|
||||
|-----------|------|-------------|
|
||||
| `b64` | `string` | Standard Base64 string |
|
||||
|
||||
**Returns:** Decoded `Uint8Array`.
|
||||
|
||||
**Throws:** `HashError("HASH.ENCODING.INVALID_INPUT")` if any character is
|
||||
outside the standard Base64 alphabet. Context: `{ encoding: "base64", input }`.
|
||||
|
||||
---
|
||||
|
||||
### `toBase64Url(bytes: Uint8Array): string`
|
||||
|
||||
Encodes `bytes` as URL-safe Base64 without padding. Uses `-` and `_` in place
|
||||
of `+` and `/`.
|
||||
|
||||
---
|
||||
|
||||
### `fromBase64Url(b64url: string): Uint8Array`
|
||||
|
||||
Decodes a URL-safe Base64 string. `+` and `/` characters are rejected.
|
||||
|
||||
**Throws:** `HashError("HASH.ENCODING.INVALID_INPUT")` on invalid input.
|
||||
Context: `{ encoding: "base64url", input }`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The encoder uses a 64-character alphabet lookup table and processes three bytes
|
||||
at a time, producing four output characters per group. The decoder validates
|
||||
every character code against a 128-entry lookup table before converting, so
|
||||
malformed input is caught in a single pass rather than producing incorrect
|
||||
output silently. The table approach avoids repeated `indexOf` calls and keeps
|
||||
decoding O(n). Standard and URL-safe alphabets share the same encode/decode
|
||||
pipeline via the `chars` and `table` parameters; `buildTable` is called once
|
||||
at module load time and the resulting `Uint8Array` tables are module-level
|
||||
constants. No `Buffer`, `btoa`, `atob`, or Node.js built-ins are used.
|
||||
@@ -0,0 +1,107 @@
|
||||
# hex
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
Provides `toHex` and `fromHex` for converting between `Uint8Array` byte
|
||||
sequences and lowercase hexadecimal strings. These functions are the standard
|
||||
representation layer for cryptographic output — digests, keys, tags, and
|
||||
derived key material are routinely logged, compared, and stored as hex strings.
|
||||
This module uses `@noble/hashes/utils` for the encoding direction and adds
|
||||
strict input validation to `fromHex` so malformed strings produce a structured
|
||||
`HashError` rather than silently corrupting data.
|
||||
|
||||
## Features
|
||||
|
||||
- `toHex(bytes)` — converts `Uint8Array` to lowercase hex string
|
||||
- `fromHex(hex)` — parses lowercase or uppercase hex; throws on malformed input
|
||||
- Throws `HashError("HASH.ENCODING.INVALID_INPUT")` on odd-length or non-hex input
|
||||
- Accepts both lowercase and uppercase hex characters on decode
|
||||
- Handles empty input correctly (empty array ↔ empty string)
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { toHex, fromHex } from "@suaveplan/crypto-hash/encoding";
|
||||
import { sha256 } from "@suaveplan/crypto-hash/hash";
|
||||
|
||||
const digest = sha256(new TextEncoder().encode("hello"));
|
||||
const hex = toHex(digest);
|
||||
console.log(hex); // 2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c...
|
||||
|
||||
const back = fromHex(hex);
|
||||
console.log(back.length); // 32
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { toHex, fromHex } from "@suaveplan/crypto-hash/encoding";
|
||||
import { hmac } from "@suaveplan/crypto-hash/mac";
|
||||
import { HashError } from "@suaveplan/crypto-hash/errors";
|
||||
|
||||
function parseAndVerifyTag(
|
||||
rawTagHex: string,
|
||||
key: Uint8Array,
|
||||
payload: Uint8Array
|
||||
): boolean {
|
||||
let receivedTag: Uint8Array;
|
||||
try {
|
||||
receivedTag = fromHex(rawTagHex);
|
||||
} catch (err) {
|
||||
if (err instanceof HashError) {
|
||||
console.error("invalid tag format:", err.context);
|
||||
return false;
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
const expected = hmac("SHA-256", key, payload);
|
||||
if (expected.length !== receivedTag.length) return false;
|
||||
let diff = 0;
|
||||
for (let i = 0; i < expected.length; i++) {
|
||||
diff |= expected[i] ^ receivedTag[i];
|
||||
}
|
||||
return diff === 0;
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `toHex(bytes: Uint8Array): string`
|
||||
|
||||
Converts `bytes` to a lowercase hexadecimal string. Each byte becomes exactly
|
||||
two hex characters; empty input returns `""`.
|
||||
|
||||
| Parameter | Type | Description |
|
||||
|-----------|------|-------------|
|
||||
| `bytes` | `Uint8Array` | Byte sequence to encode |
|
||||
|
||||
**Returns:** Lowercase hex string of length `bytes.length * 2`.
|
||||
|
||||
---
|
||||
|
||||
### `fromHex(hex: string): Uint8Array`
|
||||
|
||||
Parses a hexadecimal string (case-insensitive) into a `Uint8Array`.
|
||||
|
||||
| Parameter | Type | Description |
|
||||
|-----------|------|-------------|
|
||||
| `hex` | `string` | Hex-encoded string (may be upper- or lowercase) |
|
||||
|
||||
**Returns:** `Uint8Array` of length `hex.length / 2`.
|
||||
|
||||
**Throws:** `HashError("HASH.ENCODING.INVALID_INPUT")` if `hex.length` is odd
|
||||
or if any character is outside `[0-9a-fA-F]`. The error `context` object
|
||||
includes `{ encoding: "hex", input: <truncated input> }`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
`toHex` delegates to `bytesToHex` from `@noble/hashes/utils`, which is a
|
||||
well-tested, allocation-efficient implementation. `fromHex` validates the input
|
||||
with a regular expression before calling `hexToBytes` — this ensures that
|
||||
malformed strings throw a structured `HashError` rather than silently producing
|
||||
garbage or throwing an untyped `Error`. The validation regex matches the full
|
||||
string in one pass so there is no partial-result risk. The input is truncated to
|
||||
32 characters in the error context to avoid logging full hex-encoded secrets.
|
||||
@@ -0,0 +1,75 @@
|
||||
# Hash Error Codes
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
Defines the stable error-code registry for the `@suaveplan/crypto-hash` package using `defineCodes` from `@suaveplan/error`. Every error thrown by the hash, MAC, and KDF modules carries one of these codes, giving callers a machine-readable signal they can match without inspecting human-readable message strings.
|
||||
|
||||
The code namespace `"HASH"` is unique within the project, ensuring that error codes do not collide with codes defined by other packages such as `@suaveplan/crypto-cipher` or `@suaveplan/crypto-curve`.
|
||||
|
||||
## Features
|
||||
|
||||
- `HASH_CODES.KDF_INVALID_PARAMS` — emitted when KDF parameters (iteration count, memory cost, output length) fall below required minima.
|
||||
- `HASH_CODES.ALGORITHM_UNSUPPORTED` — emitted when a caller requests an algorithm string that is not recognised by the public dispatch layer.
|
||||
- `HASH_CODES.ENCODING_INVALID_INPUT` — emitted by encoding utilities (hex, base64) when the input byte sequence or string is not valid for the requested encoding.
|
||||
- `HashCode` union type — narrows the set of codes accepted by `HashError` constructors.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { HASH_CODES } from "@suaveplan/crypto-hash/errors";
|
||||
|
||||
// Check which code an error carries.
|
||||
import { HashError } from "@suaveplan/crypto-hash/errors";
|
||||
|
||||
function handleHashError(err: HashError) {
|
||||
if (err.code === HASH_CODES.KDF_INVALID_PARAMS) {
|
||||
console.error("KDF parameters are out of range");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { HASH_CODES, HashError } from "@suaveplan/crypto-hash/errors";
|
||||
|
||||
// Construct a typed error directly with the code registry.
|
||||
function assertIterations(count: number): void {
|
||||
if (count < 600_000) {
|
||||
throw new HashError(
|
||||
HASH_CODES.KDF_INVALID_PARAMS,
|
||||
"error.hash.kdf.invalid-params",
|
||||
{ context: { received: count, minimum: 600_000 } }
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `HASH_CODES`
|
||||
|
||||
```ts
|
||||
const HASH_CODES: {
|
||||
readonly KDF_INVALID_PARAMS: "HASH.KDF.INVALID_PARAMS";
|
||||
readonly ALGORITHM_UNSUPPORTED: "HASH.ALGORITHM.UNSUPPORTED";
|
||||
readonly ENCODING_INVALID_INPUT: "HASH.ENCODING.INVALID_INPUT";
|
||||
};
|
||||
```
|
||||
|
||||
Frozen registry object produced by `defineCodes("HASH", [...])`. The keys are screaming-snake-case identifiers; the values are the dot-separated canonical codes used in error serialisation and log queries.
|
||||
|
||||
### `HashCode`
|
||||
|
||||
```ts
|
||||
type HashCode = (typeof HASH_CODES)[keyof typeof HASH_CODES];
|
||||
```
|
||||
|
||||
Union of every string literal value in `HASH_CODES`. Used as the first type parameter to `SuaveplanError<HashCode>`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
`defineCodes` from `@suaveplan/error` takes a namespace prefix and an array of sub-codes, joins them with a period, and returns a frozen map. The frozen map is both a runtime constant (safe to use in `switch` statements) and a type-level constant (the literal values flow through to `HashCode`). Adding a new code requires extending the `as const` array and re-running the TypeScript compiler; no numeric ID assignment or migration is needed.
|
||||
@@ -0,0 +1,79 @@
|
||||
# HashError
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
`HashError` is the package-local error class for `@suaveplan/crypto-hash`. It extends `SuaveplanError<HashCode>` from `@suaveplan/error`, inheriting the full structured-error contract: machine-readable code, i18n message key, cause-chain traversal, Zod-validated wire format, and OTel span-event adaptation.
|
||||
|
||||
The class exists to widen the protected `SuaveplanError` constructor to `public`, which lets primitive implementations and public façade functions throw without needing access to the base class internals. This is the canonical pattern used across every `@suaveplan/*` package that defines its own error codes.
|
||||
|
||||
## Features
|
||||
|
||||
- Typed on `HashCode` — compiler enforces that only codes from `HASH_CODES` are passed.
|
||||
- Full cause-chain via `options.cause` — wraps lower-level errors (e.g. `PrimitiveNotImplemented`) without losing stack information.
|
||||
- Structured context via `options.context` — attach key/value metadata (algorithm name, expected vs. actual lengths) for observability.
|
||||
- Compatible with `SuaveplanError` cause-chain traversal utilities in `@suaveplan/error`.
|
||||
- Serialisable to JSON wire format for cross-boundary error transport.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { HashError } from "@suaveplan/crypto-hash/errors";
|
||||
import { HASH_CODES } from "@suaveplan/crypto-hash/errors";
|
||||
|
||||
throw new HashError(
|
||||
HASH_CODES.KDF_INVALID_PARAMS,
|
||||
"error.hash.kdf.invalid-params"
|
||||
);
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { HashError } from "@suaveplan/crypto-hash/errors";
|
||||
import { HASH_CODES } from "@suaveplan/crypto-hash/errors";
|
||||
|
||||
function assertOutputLength(length: number): void {
|
||||
if (length < 1 || length > 255 * 32) {
|
||||
throw new HashError(
|
||||
HASH_CODES.KDF_INVALID_PARAMS,
|
||||
"error.hash.kdf.invalid-params",
|
||||
{
|
||||
cause: new RangeError(`length ${length} out of bounds`),
|
||||
context: {
|
||||
field: "length",
|
||||
received: length,
|
||||
min: 1,
|
||||
max: 255 * 32,
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `class HashError extends SuaveplanError<HashCode>`
|
||||
|
||||
#### `constructor(code: HashCode, messageKey: string, options?: SuaveplanErrorOptions)`
|
||||
|
||||
| Parameter | Type | Description |
|
||||
|-----------|------|-------------|
|
||||
| `code` | `HashCode` | One of the codes from `HASH_CODES`. |
|
||||
| `messageKey` | `string` | Dotted i18n key used by the message resolver hook. |
|
||||
| `options` | `SuaveplanErrorOptions` (optional) | `cause`, `context`, and other base-class options. |
|
||||
|
||||
Inherits all public members of `SuaveplanError<HashCode>`:
|
||||
|
||||
- `code: HashCode`
|
||||
- `messageKey: string`
|
||||
- `context?: Record<string, unknown>`
|
||||
- `toJSON(): unknown` — Zod-validated wire representation.
|
||||
- `toSpanEvent(): OTelSpanEvent` — OpenTelemetry span event.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The constructor body is intentionally empty; it exists only to satisfy the TypeScript visibility requirement that converts `protected` (the base class default) to `public`. Biome's `noUselessConstructor` rule is suppressed with an inline ignore comment for this reason — the constructor is semantically required even though it contains no logic. The pattern is consistent with `CryptoCipherError` and the error classes in every other `@suaveplan/crypto-*` package.
|
||||
@@ -0,0 +1,111 @@
|
||||
# sha2
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
Provides one-shot SHA-2 digest functions (`sha256`, `sha384`, `sha512`) and a
|
||||
`hash` dispatcher that selects the algorithm by name. All functions are thin,
|
||||
allocation-friendly wrappers around the audited `@noble/hashes` implementation —
|
||||
zero WASM, zero side effects, fully tree-shakeable.
|
||||
|
||||
The module exists as an independent substrate so that higher-level packages
|
||||
(`@suaveplan/crypto-hash`, `@suaveplan/crypto`) can import SHA-2 digests without
|
||||
pulling in unrelated cryptographic primitives.
|
||||
|
||||
## Features
|
||||
|
||||
- `sha256(data)` — 32-byte SHA-256 digest
|
||||
- `sha384(data)` — 48-byte SHA-384 digest
|
||||
- `sha512(data)` — 64-byte SHA-512 digest
|
||||
- `hash(algo, data)` — single-call dispatch over `HashAlgorithm`
|
||||
- `HashAlgorithm` union type (`"SHA-256" | "SHA-384" | "SHA-512"`)
|
||||
- Deterministic: same input always produces the same output
|
||||
- Zero runtime state; safe for concurrent use
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { sha256 } from "@suaveplan/crypto-hash/hash";
|
||||
|
||||
const encoder = new TextEncoder();
|
||||
const digest = sha256(encoder.encode("hello world"));
|
||||
console.log(digest.length); // 32
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { hash, type HashAlgorithm } from "@suaveplan/crypto-hash/hash";
|
||||
import { toHex } from "@suaveplan/crypto-hash/encoding";
|
||||
|
||||
function fingerprintFile(
|
||||
algo: HashAlgorithm,
|
||||
content: Uint8Array
|
||||
): string {
|
||||
return `${algo.toLowerCase()}:${toHex(hash(algo, content))}`;
|
||||
}
|
||||
|
||||
const encoder = new TextEncoder();
|
||||
console.log(fingerprintFile("SHA-256", encoder.encode("document payload")));
|
||||
// sha-256:a591a6d40bf420404a011733cfb7b190d62c65bf0bcda32b57b277d9ad9f146e
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `sha256(data: Uint8Array): Uint8Array`
|
||||
|
||||
Computes the SHA-256 digest of `data`. Returns a fresh 32-byte `Uint8Array`.
|
||||
|
||||
| Parameter | Type | Description |
|
||||
|-----------|------|-------------|
|
||||
| `data` | `Uint8Array` | Arbitrary byte sequence to hash |
|
||||
|
||||
**Returns:** 32-byte digest.
|
||||
|
||||
---
|
||||
|
||||
### `sha384(data: Uint8Array): Uint8Array`
|
||||
|
||||
Computes the SHA-384 digest of `data`. Returns a fresh 48-byte `Uint8Array`.
|
||||
|
||||
---
|
||||
|
||||
### `sha512(data: Uint8Array): Uint8Array`
|
||||
|
||||
Computes the SHA-512 digest of `data`. Returns a fresh 64-byte `Uint8Array`.
|
||||
|
||||
---
|
||||
|
||||
### `hash(algo: HashAlgorithm, data: Uint8Array): Uint8Array`
|
||||
|
||||
Dispatch wrapper. Selects the underlying function by `algo` and returns the
|
||||
digest. Useful when the algorithm is determined at runtime.
|
||||
|
||||
| Parameter | Type | Description |
|
||||
|-----------|------|-------------|
|
||||
| `algo` | `HashAlgorithm` | `"SHA-256"`, `"SHA-384"`, or `"SHA-512"` |
|
||||
| `data` | `Uint8Array` | Arbitrary byte sequence to hash |
|
||||
|
||||
**Returns:** Variable-length digest (32 / 48 / 64 bytes).
|
||||
|
||||
---
|
||||
|
||||
### `HashAlgorithm`
|
||||
|
||||
```ts
|
||||
type HashAlgorithm = "SHA-256" | "SHA-384" | "SHA-512";
|
||||
```
|
||||
|
||||
String union that gates which algorithms the rest of the hash package accepts.
|
||||
Aligns with Web Crypto API naming conventions for cross-platform consistency.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
All three functions delegate directly to `@noble/hashes/sha2`, which is an
|
||||
extensively audited, pure-JavaScript SHA-2 implementation. The wrappers add no
|
||||
observable overhead beyond a single function call. The `hash` dispatcher uses
|
||||
two explicit `if` checks and a fallthrough to `sha512`, so the TypeScript
|
||||
exhaustiveness checker catches future additions to `HashAlgorithm` at the call
|
||||
site. No WASM is loaded; no global state is touched.
|
||||
@@ -0,0 +1,151 @@
|
||||
# argon2id
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
Provides Argon2id password hashing as specified in RFC 9106. Argon2id is the
|
||||
current winner of the Password Hashing Competition and is recommended by OWASP
|
||||
for new applications. It is resistant to both GPU-based brute force (via high
|
||||
memory cost) and time-memory trade-off attacks (via the hybrid id variant).
|
||||
This module enforces non-pathological lower bounds on all four tuning parameters
|
||||
and produces a structured `HashError` with per-field context when they are
|
||||
violated, making it safe for frameworks to call without a separate validation
|
||||
layer.
|
||||
|
||||
## Features
|
||||
|
||||
- `argon2id(password, options)` — Argon2id key derivation
|
||||
- Exported constants: `ARGON2ID_MIN_MEMORY_COST_KIB`, `ARGON2ID_MIN_TIME_COST`, `ARGON2ID_MIN_PARALLELISM`
|
||||
- Validates each parameter individually; error context includes `{ field, value, minimum }`
|
||||
- Throws `HashError("HASH.KDF.INVALID_PARAMS")` for any out-of-range parameter
|
||||
- Deterministic: same inputs always produce the same hash output
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { argon2id, ARGON2ID_MIN_MEMORY_COST_KIB } from "@suaveplan/crypto-hash/kdf";
|
||||
|
||||
const encoder = new TextEncoder();
|
||||
const password = encoder.encode("hunter2");
|
||||
const salt = new Uint8Array(16); // use crypto.getRandomValues in production
|
||||
|
||||
const hash = argon2id(password, {
|
||||
salt,
|
||||
timeCost: 3,
|
||||
memoryCost: ARGON2ID_MIN_MEMORY_COST_KIB * 1024, // 8 MiB in KiB
|
||||
parallelism: 1,
|
||||
length: 32,
|
||||
});
|
||||
console.log(hash.length); // 32
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import {
|
||||
argon2id,
|
||||
ARGON2ID_MIN_MEMORY_COST_KIB,
|
||||
ARGON2ID_MIN_TIME_COST,
|
||||
ARGON2ID_MIN_PARALLELISM,
|
||||
} from "@suaveplan/crypto-hash/kdf";
|
||||
import { toHex } from "@suaveplan/crypto-hash/encoding";
|
||||
import { HashError } from "@suaveplan/crypto-hash/errors";
|
||||
|
||||
interface PasswordHashConfig {
|
||||
timeCost: number;
|
||||
memoryCostKib: number;
|
||||
parallelism: number;
|
||||
outputBytes: number;
|
||||
}
|
||||
|
||||
function hashPassword(password: string, salt: Uint8Array, cfg: PasswordHashConfig): string {
|
||||
try {
|
||||
const raw = argon2id(new TextEncoder().encode(password), {
|
||||
salt,
|
||||
timeCost: cfg.timeCost,
|
||||
memoryCost: cfg.memoryCostKib,
|
||||
parallelism: cfg.parallelism,
|
||||
length: cfg.outputBytes,
|
||||
});
|
||||
return toHex(raw);
|
||||
} catch (err) {
|
||||
if (err instanceof HashError && err.context?.field) {
|
||||
throw new RangeError(
|
||||
`Argon2id parameter out of range: ${err.context.field} = ${err.context.value} ` +
|
||||
`(minimum: ${err.context.minimum})`
|
||||
);
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
const salt = new Uint8Array(16);
|
||||
const hex = hashPassword("secure-password", salt, {
|
||||
timeCost: ARGON2ID_MIN_TIME_COST,
|
||||
memoryCostKib: ARGON2ID_MIN_MEMORY_COST_KIB,
|
||||
parallelism: ARGON2ID_MIN_PARALLELISM,
|
||||
outputBytes: 32,
|
||||
});
|
||||
console.log(hex.length); // 64 hex chars = 32 bytes
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `argon2id(password: Uint8Array, options: Argon2idOptions): Uint8Array`
|
||||
|
||||
Derives `options.length` bytes from `password` using Argon2id.
|
||||
|
||||
| Parameter | Type | Description |
|
||||
|-----------|------|-------------|
|
||||
| `password` | `Uint8Array` | User-supplied password bytes |
|
||||
| `options.salt` | `Uint8Array` | Per-credential random salt (≥ 16 bytes recommended) |
|
||||
| `options.timeCost` | `number` | Integer ≥ `ARGON2ID_MIN_TIME_COST` (number of iterations) |
|
||||
| `options.memoryCost` | `number` | Integer ≥ `ARGON2ID_MIN_MEMORY_COST_KIB` (in KiB) |
|
||||
| `options.parallelism` | `number` | Integer ≥ `ARGON2ID_MIN_PARALLELISM` (lane count) |
|
||||
| `options.length` | `number` | Positive integer byte count for derived output |
|
||||
|
||||
**Returns:** `Uint8Array` of `options.length` bytes.
|
||||
|
||||
**Throws:** `HashError("HASH.KDF.INVALID_PARAMS")` for any validation failure.
|
||||
Error `context` contains `{ field: string, value: number, minimum: number }`.
|
||||
|
||||
---
|
||||
|
||||
### `ARGON2ID_MIN_MEMORY_COST_KIB = 8`
|
||||
|
||||
Minimum memory cost in KiB. Based on RFC 9106 §4 minimum recommendation.
|
||||
|
||||
### `ARGON2ID_MIN_TIME_COST = 1`
|
||||
|
||||
Minimum number of Argon2id iterations. Lower values are pathological.
|
||||
|
||||
### `ARGON2ID_MIN_PARALLELISM = 1`
|
||||
|
||||
Minimum lane count. Lower values are rejected as invalid.
|
||||
|
||||
---
|
||||
|
||||
### `Argon2idOptions`
|
||||
|
||||
```ts
|
||||
interface Argon2idOptions {
|
||||
readonly salt: Uint8Array;
|
||||
readonly timeCost: number;
|
||||
readonly memoryCost: number;
|
||||
readonly parallelism: number;
|
||||
readonly length: number;
|
||||
}
|
||||
```
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
Parameters are validated in order: `timeCost`, `memoryCost`, `parallelism`,
|
||||
`length`. The first failing field short-circuits and throws with that field's
|
||||
context — no partial execution occurs. The implementation delegates to
|
||||
`@noble/hashes/argon2` which is a pure-JavaScript Argon2id implementation
|
||||
reviewed by the cryptographic community. Argon2id does not use the `HashAlgorithm`
|
||||
dispatch pattern used by `hmac`, `hkdf`, and `pbkdf2` — Argon2id's internal
|
||||
structure is fixed and parameterised by `t`, `m`, and `p` rather than a hash
|
||||
constructor. For this reason, `argon2id.ts` does not import from `mac/hmac.ts`.
|
||||
@@ -0,0 +1,106 @@
|
||||
# hkdf
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
Provides HKDF (HMAC-based Key Derivation Function, RFC 5869) over SHA-256,
|
||||
SHA-384, and SHA-512. HKDF is the standard mechanism for stretching or
|
||||
diversifying key material — for instance, turning a Diffie-Hellman shared
|
||||
secret into one or more symmetric encryption keys. This module wraps the
|
||||
audited `@noble/hashes` HKDF implementation and constrains the algorithm
|
||||
surface to the `HashAlgorithm` union shared across `@suaveplan/crypto-hash`.
|
||||
|
||||
## Features
|
||||
|
||||
- `hkdf(algo, ikm, options)` — HKDF-Extract-and-Expand in one call
|
||||
- Supports SHA-256, SHA-384, SHA-512 via `HashAlgorithm`
|
||||
- Configurable output length (in bytes)
|
||||
- Accepts arbitrary `salt` and `info` context bytes
|
||||
- Deterministic: same inputs always produce the same key material
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { hkdf } from "@suaveplan/crypto-hash/kdf";
|
||||
|
||||
const ikm = new Uint8Array(32).fill(0x01); // input key material
|
||||
const salt = new Uint8Array(16); // random salt (often public)
|
||||
const info = new TextEncoder().encode("suaveplan-session-key-v1");
|
||||
|
||||
const sessionKey = hkdf("SHA-256", ikm, { salt, info, length: 32 });
|
||||
console.log(sessionKey.length); // 32
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { hkdf } from "@suaveplan/crypto-hash/kdf";
|
||||
import { toHex } from "@suaveplan/crypto-hash/encoding";
|
||||
|
||||
// Derive separate encryption and MAC keys from a single DH shared secret
|
||||
function deriveKeyPair(sharedSecret: Uint8Array, context: string): {
|
||||
encKey: Uint8Array;
|
||||
macKey: Uint8Array;
|
||||
} {
|
||||
const encoder = new TextEncoder();
|
||||
const salt = new Uint8Array(32); // zero salt; real usage uses random
|
||||
|
||||
const encKey = hkdf("SHA-256", sharedSecret, {
|
||||
salt,
|
||||
info: encoder.encode(`${context}:enc`),
|
||||
length: 32,
|
||||
});
|
||||
const macKey = hkdf("SHA-256", sharedSecret, {
|
||||
salt,
|
||||
info: encoder.encode(`${context}:mac`),
|
||||
length: 32,
|
||||
});
|
||||
|
||||
return { encKey, macKey };
|
||||
}
|
||||
|
||||
const { encKey, macKey } = deriveKeyPair(new Uint8Array(32), "chat-session");
|
||||
console.log(toHex(encKey));
|
||||
console.log(toHex(macKey));
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `hkdf(algo: HashAlgorithm, ikm: Uint8Array, options: HkdfOptions): Uint8Array`
|
||||
|
||||
Derives `options.length` bytes of key material from the input keying material
|
||||
`ikm` using the two-step HKDF-Extract-and-Expand procedure.
|
||||
|
||||
| Parameter | Type | Description |
|
||||
|-----------|------|-------------|
|
||||
| `algo` | `HashAlgorithm` | Underlying hash: `"SHA-256"`, `"SHA-384"`, or `"SHA-512"` |
|
||||
| `ikm` | `Uint8Array` | Input keying material (e.g., DH shared secret) |
|
||||
| `options.salt` | `Uint8Array` | Optional salt bytes (use a random or domain-specific value) |
|
||||
| `options.info` | `Uint8Array` | Context and application-specific information |
|
||||
| `options.length` | `number` | Number of output bytes to derive |
|
||||
|
||||
**Returns:** `Uint8Array` of `options.length` bytes.
|
||||
|
||||
---
|
||||
|
||||
### `HkdfOptions`
|
||||
|
||||
```ts
|
||||
interface HkdfOptions {
|
||||
readonly salt: Uint8Array;
|
||||
readonly info: Uint8Array;
|
||||
readonly length: number;
|
||||
}
|
||||
```
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
HKDF is defined in RFC 5869. The Extract step computes `PRK = HMAC(salt, ikm)`;
|
||||
the Expand step iterates `HMAC(PRK, info || counter)` until `length` bytes are
|
||||
produced. Both steps are performed by `@noble/hashes/hkdf` — no hand-rolled
|
||||
HMAC is involved here. The internal `hasherFor` helper is a private module
|
||||
function mapping `HashAlgorithm` strings to hash constructors; it is not
|
||||
exported. The maximum derivable length is `255 × hashLen`, which for SHA-256 is
|
||||
8160 bytes — well within any practical keying requirement.
|
||||
@@ -0,0 +1,128 @@
|
||||
# pbkdf2
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
Provides PBKDF2 (Password-Based Key Derivation Function 2, RFC 8018) over
|
||||
SHA-2 variants. PBKDF2 derives a fixed-length key from a password by applying
|
||||
an HMAC pseudo-random function iteratively — the iteration count is the primary
|
||||
cost-tuning knob. This module enforces a hard floor of 600 000 iterations
|
||||
(the OWASP 2023 minimum for PBKDF2-SHA-256) to prevent accidental use of
|
||||
dangerously weak parameters. Callers that legitimately need lower counts for
|
||||
legacy-system interoperability can call `@noble/hashes` directly.
|
||||
|
||||
## Features
|
||||
|
||||
- `pbkdf2(algo, password, options)` — PBKDF2 over SHA-256/384/512
|
||||
- `PBKDF2_MIN_ITERATIONS = 600_000` — OWASP 2023 minimum, exported as a constant
|
||||
- Throws `HashError("HASH.KDF.INVALID_PARAMS")` if `iterations < 600_000` or non-integer
|
||||
- Throws `HashError("HASH.KDF.INVALID_PARAMS")` if `length ≤ 0` or non-integer
|
||||
- Error context includes `{ field, value, minimum }` for programmatic handling
|
||||
- Deterministic: same inputs always produce the same derived key
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { pbkdf2, PBKDF2_MIN_ITERATIONS } from "@suaveplan/crypto-hash/kdf";
|
||||
|
||||
const encoder = new TextEncoder();
|
||||
const password = encoder.encode("correct-horse-battery-staple");
|
||||
const salt = new Uint8Array(16); // use crypto.getRandomValues in real code
|
||||
|
||||
const key = pbkdf2("SHA-256", password, {
|
||||
salt,
|
||||
iterations: PBKDF2_MIN_ITERATIONS,
|
||||
length: 32,
|
||||
});
|
||||
console.log(key.length); // 32
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { pbkdf2, PBKDF2_MIN_ITERATIONS } from "@suaveplan/crypto-hash/kdf";
|
||||
import { toHex } from "@suaveplan/crypto-hash/encoding";
|
||||
import { HashError } from "@suaveplan/crypto-hash/errors";
|
||||
|
||||
function deriveStorageKey(
|
||||
password: string,
|
||||
saltHex: string,
|
||||
lengthBytes = 32
|
||||
): string {
|
||||
const encoder = new TextEncoder();
|
||||
const salt = new Uint8Array(
|
||||
saltHex.match(/.{2}/g)!.map((b) => parseInt(b, 16))
|
||||
);
|
||||
|
||||
try {
|
||||
const key = pbkdf2("SHA-256", encoder.encode(password), {
|
||||
salt,
|
||||
iterations: PBKDF2_MIN_ITERATIONS,
|
||||
length: lengthBytes,
|
||||
});
|
||||
return toHex(key);
|
||||
} catch (err) {
|
||||
if (err instanceof HashError) {
|
||||
throw new Error(
|
||||
`Key derivation failed: ${err.code} — ${JSON.stringify(err.context)}`
|
||||
);
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `pbkdf2(algo: HashAlgorithm, password: Uint8Array, options: Pbkdf2Options): Uint8Array`
|
||||
|
||||
Derives `options.length` bytes from `password` using PBKDF2.
|
||||
|
||||
| Parameter | Type | Description |
|
||||
|-----------|------|-------------|
|
||||
| `algo` | `HashAlgorithm` | Underlying hash: `"SHA-256"`, `"SHA-384"`, or `"SHA-512"` |
|
||||
| `password` | `Uint8Array` | User-supplied password bytes |
|
||||
| `options.salt` | `Uint8Array` | Per-credential random salt (≥ 16 bytes recommended) |
|
||||
| `options.iterations` | `number` | Integer ≥ `PBKDF2_MIN_ITERATIONS` |
|
||||
| `options.length` | `number` | Positive integer byte count for derived key |
|
||||
|
||||
**Returns:** `Uint8Array` of `options.length` bytes.
|
||||
|
||||
**Throws:** `HashError("HASH.KDF.INVALID_PARAMS")` when `iterations` or `length`
|
||||
fail validation. The error `context` object includes `{ field, value, minimum? }`.
|
||||
|
||||
---
|
||||
|
||||
### `PBKDF2_MIN_ITERATIONS`
|
||||
|
||||
```ts
|
||||
const PBKDF2_MIN_ITERATIONS = 600_000;
|
||||
```
|
||||
|
||||
OWASP 2023 recommended minimum for PBKDF2-SHA-256. Exported so callers can
|
||||
reference it when building configuration objects.
|
||||
|
||||
---
|
||||
|
||||
### `Pbkdf2Options`
|
||||
|
||||
```ts
|
||||
interface Pbkdf2Options {
|
||||
readonly salt: Uint8Array;
|
||||
readonly iterations: number;
|
||||
readonly length: number;
|
||||
}
|
||||
```
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
PBKDF2 is defined in RFC 8018 (superseding RFC 2898). The function delegates
|
||||
to `@noble/hashes/pbkdf2` and maps `HashAlgorithm` strings to hash constructors
|
||||
via a private `hasherFor` function that is not exported. The 600 000-iteration
|
||||
floor corresponds to OWASP's 2023 recommendation for PBKDF2-HMAC-SHA-256; the
|
||||
NIST SP 800-132 guideline is 10 000 but OWASP's figure accounts for modern GPU
|
||||
attack capabilities. Applications using SHA-384 or SHA-512 may consider lower
|
||||
counts because those hashes are slower on commodity hardware, but this module
|
||||
applies the same floor for all variants to avoid subtle configuration mistakes.
|
||||
@@ -0,0 +1,69 @@
|
||||
# argon2id-password
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
`argon2id-password.ts` provides a PHC-format password hashing scheme on top of the Argon2id primitive. The output format is `$argon2id$v=19$m=<memoryCost>,t=<timeCost>,p=<parallelism>$<saltB64>$<hashB64>` — the official Argon2id PHC string used by `argon2`-cli, libsodium, and most server-side authentication libraries. suaveplan emits the same format so vault tooling, server keystores, and migration scripts can interoperate without an adapter layer.
|
||||
|
||||
Argon2id is the recommended OWASP password hashing scheme. It is memory-hard (resistant to GPU/ASIC brute force), time-hard (configurable iteration count), and side-channel resistant (the "id" variant blends data-independent and data-dependent passes). This module wraps the suaveplan Argon2id primitive with the PHC string format, default OWASP-2023 parameters, and constant-time verification.
|
||||
|
||||
## Features
|
||||
|
||||
- PHC-format output `$argon2id$v=19$m=N,t=N,p=N$saltB64$hashB64` — interoperable with all standard Argon2id implementations.
|
||||
- OWASP 2023 default parameters: `m=65536` (64 MiB), `t=3` iterations, `p=1` parallelism.
|
||||
- Constant-time verification via `@suaveplan/crypto-utils::constantTimeEqual` over the 32-byte hash.
|
||||
- 16-byte fresh random salt per hash call; deterministic when an explicit salt is supplied (test-only).
|
||||
- Strict format validation: malformed strings throw `HashError(BLAKE2B_PASSWORD_FORMAT_INVALID)` (note: the same generic password-format error code is shared between Argon2id and BLAKE2b passwords; the message key disambiguates).
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import {
|
||||
hashPasswordArgon2id,
|
||||
verifyPasswordArgon2id,
|
||||
} from "@suaveplan/crypto-hash/mac/argon2id-password/argon2id-password";
|
||||
|
||||
const stored = hashPasswordArgon2id("correct horse battery staple");
|
||||
// → "$argon2id$v=19$m=65536,t=3,p=1$<salt>$<hash>"
|
||||
|
||||
const ok = verifyPasswordArgon2id(stored, "correct horse battery staple");
|
||||
// → true
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { hashPasswordArgon2id, verifyPasswordArgon2id } from "@suaveplan/crypto-hash/mac/argon2id-password/argon2id-password";
|
||||
|
||||
// Adaptive cost: probe how long a verify takes on this hardware and
|
||||
// pick the largest memory cost that still fits in a target latency.
|
||||
function calibrate(password: string, targetMs: number): number {
|
||||
let m = 16_384; // 16 MiB starting point
|
||||
while (m <= 262_144) {
|
||||
const stored = hashPasswordArgon2id(password, {
|
||||
memoryCost: m,
|
||||
timeCost: 3,
|
||||
parallelism: 1,
|
||||
});
|
||||
const start = performance.now();
|
||||
verifyPasswordArgon2id(stored, password);
|
||||
if (performance.now() - start > targetMs) return m / 2;
|
||||
m *= 2;
|
||||
}
|
||||
return 262_144;
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `hashPasswordArgon2id(password: string, options?: HashPasswordArgon2idOptions): string`.
|
||||
- `verifyPasswordArgon2id(storedHash: string, password: string): boolean`.
|
||||
- `HashPasswordArgon2idOptions { memoryCost?, timeCost?, parallelism?, salt? }` — all fields optional; defaults are OWASP 2023 recommendations.
|
||||
|
||||
Errors: `HASH.BLAKE2B_PASSWORD_FORMAT_INVALID` for malformed stored hashes (missing prefix, wrong segment count, bad params, bad base64).
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The PHC format uses base64-without-padding (per RFC 9106 Appendix B); the parser pads back to a multiple of 4 before decoding. Memory cost is in KiB, time cost is the number of passes, parallelism is the number of lanes — these are the same units the underlying Argon2id primitive consumes, so there is no unit conversion. Constant-time comparison happens on the raw 32-byte hash buffer; the salt and parameter components of the PHC string are not secret and do not need timing-safe handling. suaveplan re-uses the existing `BLAKE2B_PASSWORD_FORMAT_INVALID` error code rather than minting a new `ARGON2ID_*` code; both schemes are MCF-style password hashes and a unified format-error code keeps consumer dispatch simple.
|
||||
@@ -0,0 +1,72 @@
|
||||
# blake2b-password
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
`blake2b-password.ts` provides a password-hashing scheme built on the BLAKE2b primitive. It is the right tool when you need a fast, deterministic password hasher in a memory-constrained or platform-restricted environment where Argon2id is not available — for example, embedded contexts, WASM-cold-start-sensitive paths, or when interoperating with legacy systems that already store BLAKE2b-iterated password hashes.
|
||||
|
||||
The format is `$blake2b$v=1$cf=<costFactor>$<saltHex>$<hashHex>` — an MCF-style stored hash that captures the cost factor, salt, and hash bytes in a single deterministically-parseable string. The cost factor controls iteration count exponentially (`iterations = 2^costFactor`) so doubling it doubles the work; the default of `14` corresponds to 16,384 iterations of BLAKE2b-512.
|
||||
|
||||
This is **not a substitute for Argon2id**. Argon2id is memory-hard; BLAKE2b iteration is purely time-hard, which is weaker against attackers with massive parallel hardware (GPUs, FPGAs). Use Argon2id whenever you can; reach for this only when interop or platform constraints force it.
|
||||
|
||||
## Features
|
||||
|
||||
- Compact MCF-style stored format: `$blake2b$v=1$cf=N$salt$hash` — captures the algorithm tag, version, cost factor, salt, and hash in a single round-trippable string.
|
||||
- `hashPasswordBlake2b(password, options?)` — generates a fresh 16-byte salt by default; deterministic when an explicit salt is supplied.
|
||||
- `verifyPasswordBlake2b(stored, password)` — constant-time comparison via `@suaveplan/crypto-utils::constantTimeEqual`.
|
||||
- Cost-factor range `1..31` (iteration counts 2..2^31). Outside that range, verification throws `HashError(BLAKE2B_PASSWORD_FORMAT_INVALID)`.
|
||||
- Returns `false` for non-matching passwords without leaking timing information; throws structured errors only for malformed stored hashes.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import {
|
||||
hashPasswordBlake2b,
|
||||
verifyPasswordBlake2b,
|
||||
} from "@suaveplan/crypto-hash/mac/blake2b-password";
|
||||
|
||||
const stored = hashPasswordBlake2b("correct horse battery staple");
|
||||
// → "$blake2b$v=1$cf=14$<salt-hex>$<hash-hex>"
|
||||
|
||||
const ok = verifyPasswordBlake2b(stored, "correct horse battery staple");
|
||||
// → true
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { hashPasswordBlake2b, verifyPasswordBlake2b } from "@suaveplan/crypto-hash/mac/blake2b-password";
|
||||
|
||||
// Adaptive cost factor: probe how long a verify takes and tune cost.
|
||||
async function calibrate(targetMs = 100): Promise<number> {
|
||||
for (let cf = 12; cf <= 20; cf++) {
|
||||
const stored = hashPasswordBlake2b("probe", { costFactor: cf });
|
||||
const start = performance.now();
|
||||
verifyPasswordBlake2b(stored, "probe");
|
||||
if (performance.now() - start > targetMs) return cf;
|
||||
}
|
||||
return 20;
|
||||
}
|
||||
|
||||
// Migrating a password that was stored at an older cost factor: detect
|
||||
// the cost in the stored string and re-hash on successful login.
|
||||
function needsRehash(storedHash: string, target: number): boolean {
|
||||
const match = storedHash.match(/\$cf=(\d+)\$/);
|
||||
return match ? Number(match[1]) < target : true;
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `hashPasswordBlake2b(password: string, options?: HashPasswordBlake2bOptions): string` — produces an MCF-style stored hash.
|
||||
- `verifyPasswordBlake2b(storedHash: string, password: string): boolean` — constant-time verify.
|
||||
- `HashPasswordBlake2bOptions { costFactor?: number; salt?: Uint8Array }` — `costFactor` defaults to `14`; `salt` defaults to a fresh 16-byte random buffer.
|
||||
- `DEFAULT_COST_FACTOR = 14`, `SALT_LENGTH = 16`, `HASH_LENGTH = 64` constants.
|
||||
|
||||
Errors: `HASH.BLAKE2B_PASSWORD_FORMAT_INVALID` for malformed stored hashes (missing prefix, wrong segment count, missing `cf=` tag, out-of-range cost factor, malformed hex, wrong hash length).
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The iteration loop is BLAKE2b chained on its own output: `acc_0 = BLAKE2b(password ‖ salt)`, `acc_n = BLAKE2b(acc_{n-1})`. This is intentionally simple — it's not a memory-hard scheme like Argon2id, just an iterated hash. The cost factor is exponential (`iterations = 2^costFactor`) so the default of `14` gives 16,384 iterations, which is approximately `~5–10ms` on a modern desktop and `~50ms` on a slow mobile. Constant-time comparison happens on the raw 64-byte hash buffer via `constantTimeEqual`, which means the verify side leaks no information about which byte first differs. The cost factor is bounded at 31 to prevent integer-overflow surprises (`1 << 32` overflows to negative in JS bitwise math). Salts are caller-supplied via the `salt` option for tests; the default is `randomBytes(16)`.
|
||||
@@ -0,0 +1,97 @@
|
||||
# hmac
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
Provides HMAC (Hash-based Message Authentication Code) over SHA-2 variants.
|
||||
HMAC proves that a message was produced by a party in possession of a shared
|
||||
key, giving both data integrity and authenticity guarantees without asymmetric
|
||||
cryptography. This module wraps the audited `@noble/hashes` HMAC primitive
|
||||
and restricts the algorithm surface to the same `HashAlgorithm` union used
|
||||
throughout `@suaveplan/crypto-hash`.
|
||||
|
||||
## Features
|
||||
|
||||
- `hmac(algo, key, data)` — one-shot HMAC-SHA-{256,384,512}
|
||||
- Throws `HashError("HASH.ALGORITHM.UNSUPPORTED")` for unrecognised algorithm strings
|
||||
- Passes through RFC 4231 HMAC-SHA-256 test vectors
|
||||
- Deterministic: same `(algo, key, data)` triple always produces the same tag
|
||||
- Zero runtime state; safe for concurrent use
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { hmac } from "@suaveplan/crypto-hash/mac";
|
||||
import { toHex } from "@suaveplan/crypto-hash/encoding";
|
||||
|
||||
const encoder = new TextEncoder();
|
||||
const key = encoder.encode("super-secret-key");
|
||||
const data = encoder.encode("payment:amount=100");
|
||||
|
||||
const tag = hmac("SHA-256", key, data);
|
||||
console.log(toHex(tag)); // 32-byte hex string
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { hmac } from "@suaveplan/crypto-hash/mac";
|
||||
import { toHex, fromHex } from "@suaveplan/crypto-hash/encoding";
|
||||
|
||||
// Verify a webhook signature in constant time
|
||||
function verifyWebhookTag(
|
||||
secret: Uint8Array,
|
||||
payload: Uint8Array,
|
||||
receivedTagHex: string
|
||||
): boolean {
|
||||
const expected = hmac("SHA-256", secret, payload);
|
||||
const received = fromHex(receivedTagHex);
|
||||
if (expected.length !== received.length) return false;
|
||||
let diff = 0;
|
||||
for (let i = 0; i < expected.length; i++) {
|
||||
diff |= expected[i] ^ received[i];
|
||||
}
|
||||
return diff === 0;
|
||||
}
|
||||
|
||||
const encoder = new TextEncoder();
|
||||
const secret = encoder.encode("webhook-secret");
|
||||
const body = encoder.encode('{"event":"charge.succeeded"}');
|
||||
const tag = toHex(hmac("SHA-256", secret, body));
|
||||
console.log(verifyWebhookTag(secret, body, tag)); // true
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `hmac(algo: HashAlgorithm, key: Uint8Array, data: Uint8Array): Uint8Array`
|
||||
|
||||
Computes the HMAC of `data` under `key` using the specified SHA-2 algorithm.
|
||||
|
||||
| Parameter | Type | Description |
|
||||
|-----------|------|-------------|
|
||||
| `algo` | `HashAlgorithm` | `"SHA-256"`, `"SHA-384"`, or `"SHA-512"` |
|
||||
| `key` | `Uint8Array` | Shared secret key (any length ≥ 1 byte) |
|
||||
| `data` | `Uint8Array` | Message to authenticate |
|
||||
|
||||
**Returns:** Tag bytes — 32 / 48 / 64 bytes for SHA-256 / SHA-384 / SHA-512
|
||||
respectively.
|
||||
|
||||
**Throws:** `HashError("HASH.ALGORITHM.UNSUPPORTED")` if `algo` is not one of
|
||||
the three supported variants.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The internal `hasherFor` helper maps `HashAlgorithm` strings to the corresponding
|
||||
`@noble/hashes` hash constructor. It is a private module function — not exported
|
||||
from this file or from any barrel — so it cannot be called by consumers.
|
||||
This design keeps the public API surface minimal while allowing `hkdf.ts` and
|
||||
`pbkdf2.ts` to import `hasherFor` from this module directly for their own
|
||||
internal use. `argon2id.ts` does not use `hasherFor` because Argon2id is a
|
||||
password-hashing algorithm that accepts its own internal parameterisation rather
|
||||
than a bare hash constructor.
|
||||
|
||||
HMAC output length equals the underlying hash output length. The `@noble/hashes`
|
||||
implementation is constant-time within the hash primitive itself; callers are
|
||||
responsible for constant-time comparison of the resulting tags.
|
||||
@@ -0,0 +1,98 @@
|
||||
# argon2id.select
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
`argon2id.select.ts` is the build-time primitive selector for the Argon2id implementation in `@suaveplan/crypto-hash`. It re-exports one concrete backend — either `argon2id.noble.ts` (backed by `@noble/hashes`) or `argon2id.native.ts` — based on the `argon2id` entry in `crypto.config.ts`.
|
||||
|
||||
Argon2id is the Password Hashing Competition winner and the recommendation of RFC 9106 for password hashing and key derivation. Its hybrid data-dependent and data-independent memory access pattern resists both GPU-parallel and side-channel attacks, making it the preferred choice when deriving keys from passwords or other low-entropy secrets. The selector exposes `argon2id` and `Argon2idOptions` through a stable interface.
|
||||
|
||||
## Features
|
||||
|
||||
- Re-exports `argon2id(password, options)` and `Argon2idOptions`.
|
||||
- Generated by `bun scripts/gen-crypto-selects.ts` — never hand-edited.
|
||||
- Controlled by `crypto.config.ts`: set `argon2id` to `'noble'` or `'native'`.
|
||||
- `Argon2idOptions` carries `salt`, `timeCost`, `memoryCost`, `parallelism`, and `length`.
|
||||
- RFC 9106 minimum parameter validation is enforced at the public façade, not here.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { argon2id } from "@suaveplan/crypto-hash/primitives/argon2id/argon2id";
|
||||
|
||||
const password = new TextEncoder().encode("correct-horse-battery-staple");
|
||||
const salt = crypto.getRandomValues(new Uint8Array(16));
|
||||
const hash = argon2id(password, {
|
||||
salt,
|
||||
timeCost: 3,
|
||||
memoryCost: 65536,
|
||||
parallelism: 4,
|
||||
length: 32,
|
||||
});
|
||||
console.log(hash.length); // 32
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { argon2id, type Argon2idOptions } from "@suaveplan/crypto-hash/primitives/argon2id/argon2id";
|
||||
|
||||
// Interactive login profile (RFC 9106 minimum-t=1 adjusted for latency).
|
||||
const INTERACTIVE_PARAMS: Argon2idOptions = {
|
||||
salt: new Uint8Array(16), // placeholder; real callers supply random salt
|
||||
timeCost: 2,
|
||||
memoryCost: 19456, // ~19 MiB
|
||||
parallelism: 1,
|
||||
length: 32,
|
||||
};
|
||||
|
||||
// Server-side profile (higher security, longer wait acceptable).
|
||||
const SENSITIVE_PARAMS: Argon2idOptions = {
|
||||
salt: new Uint8Array(16),
|
||||
timeCost: 3,
|
||||
memoryCost: 65536, // ~64 MiB
|
||||
parallelism: 4,
|
||||
length: 64,
|
||||
};
|
||||
|
||||
function deriveKey(password: Uint8Array, opts: Argon2idOptions): Uint8Array {
|
||||
return argon2id(password, opts);
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
Re-exports the following from the active implementation:
|
||||
|
||||
### `argon2id(password: Uint8Array, options: Argon2idOptions): Uint8Array`
|
||||
|
||||
Derives a key from a password using the Argon2id algorithm.
|
||||
|
||||
| Parameter | Type | Description |
|
||||
|-----------|------|-------------|
|
||||
| `password` | `Uint8Array` | Password bytes. |
|
||||
| `options.salt` | `Uint8Array` | Random salt (≥16 bytes per RFC 9106). |
|
||||
| `options.timeCost` | `number` | Number of passes over memory (≥1). |
|
||||
| `options.memoryCost` | `number` | Memory usage in KiB (≥8 × parallelism). |
|
||||
| `options.parallelism` | `number` | Degree of parallelism (≥1). |
|
||||
| `options.length` | `number` | Output length in bytes (≥4). |
|
||||
|
||||
**Returns:** Derived key of `options.length` bytes.
|
||||
|
||||
### `Argon2idOptions`
|
||||
|
||||
```ts
|
||||
interface Argon2idOptions {
|
||||
readonly salt: Uint8Array;
|
||||
readonly timeCost: number;
|
||||
readonly memoryCost: number;
|
||||
readonly parallelism: number;
|
||||
readonly length: number;
|
||||
}
|
||||
```
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
Noble's Argon2id implementation is a pure-JavaScript port that runs synchronously. It passes RFC 9106 Appendix B test vectors. Memory consumption scales linearly with `memoryCost` in KiB, so high memory costs can cause out-of-memory errors in constrained environments (e.g. edge workers). The primitive does not enforce RFC 9106 parameter minima — those checks are delegated to the public KDF façade. The native stub throws `PrimitiveNotImplemented("argon2id")`. Unlike PBKDF2, Argon2id is inherently sequential in its memory access, which limits parallelism-based GPU attacks but also means JavaScript's single-threaded nature does not significantly disadvantage it relative to native binaries.
|
||||
@@ -0,0 +1,83 @@
|
||||
# argon2id.native
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
`argon2id.native.ts` is the native-backend placeholder for the Argon2id primitive in `@suaveplan/crypto-hash`. It exports the same `argon2id` function and `Argon2idOptions` interface as the Noble implementation but throws `PrimitiveNotImplemented("argon2id")` unconditionally.
|
||||
|
||||
This stub completes the two-implementation selector layout for Argon2id. Argon2id's native backend is the most valuable of the placeholders because a native implementation could run Argon2id in a separate thread and avoid blocking the event loop, which the pure-JavaScript Noble implementation cannot do. The stub communicates that this capability is planned but not yet available.
|
||||
|
||||
## Features
|
||||
|
||||
- Satisfies `argon2id(password, options): Uint8Array` interface.
|
||||
- Exports `Argon2idOptions` (identical to Noble file) for selector re-export.
|
||||
- Throws `PrimitiveNotImplemented("argon2id")` unconditionally.
|
||||
- Zero runtime dependencies beyond `@suaveplan/crypto-utils/errors`.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
// Active only when crypto.config.ts sets argon2id to 'native'.
|
||||
import { argon2id } from "@suaveplan/crypto-hash/primitives/argon2id/argon2id";
|
||||
|
||||
try {
|
||||
argon2id(new Uint8Array(8), {
|
||||
salt: new Uint8Array(16),
|
||||
timeCost: 1,
|
||||
memoryCost: 8192,
|
||||
parallelism: 1,
|
||||
length: 32,
|
||||
});
|
||||
} catch (err) {
|
||||
// PrimitiveNotImplemented: argon2id
|
||||
console.error(err);
|
||||
}
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import type { Argon2idOptions } from "@suaveplan/crypto-hash/primitives/argon2id/argon2id";
|
||||
|
||||
// Build a typed options object to pass to whichever backend is active.
|
||||
function interactiveParams(salt: Uint8Array): Argon2idOptions {
|
||||
return {
|
||||
salt,
|
||||
timeCost: 2,
|
||||
memoryCost: 19456,
|
||||
parallelism: 1,
|
||||
length: 32,
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `argon2id(_password: Uint8Array, _options: Argon2idOptions): Uint8Array`
|
||||
|
||||
Placeholder that always throws.
|
||||
|
||||
| Parameter | Type | Description |
|
||||
|-----------|------|-------------|
|
||||
| `_password` | `Uint8Array` | Ignored. |
|
||||
| `_options` | `Argon2idOptions` | Ignored. |
|
||||
|
||||
**Throws:** `PrimitiveNotImplemented("argon2id")` — always.
|
||||
|
||||
### `Argon2idOptions`
|
||||
|
||||
```ts
|
||||
interface Argon2idOptions {
|
||||
readonly salt: Uint8Array;
|
||||
readonly timeCost: number;
|
||||
readonly memoryCost: number;
|
||||
readonly parallelism: number;
|
||||
readonly length: number;
|
||||
}
|
||||
```
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The future native implementation is expected to call into `libsodium` or a dedicated Argon2 native module to run the memory-hard computation off the JavaScript event loop. When that implementation lands and passes RFC 9106 Appendix B test vectors via the KAT harness, this file is updated, parameter prefixes removed, and the selector regenerated to point here. The `Argon2idOptions` interface is self-contained in this file so both backends are independent — the selector re-exports from whichever is active.
|
||||
@@ -0,0 +1,88 @@
|
||||
# argon2id.noble
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
`argon2id.noble.ts` is the Noble-backed concrete implementation of Argon2id in `@suaveplan/crypto-hash`. It wraps `argon2id` from `@noble/hashes/argon2`, translating the `Argon2idOptions` struct into Noble's parameter object (`t`, `m`, `p`, `dkLen`).
|
||||
|
||||
Argon2id (RFC 9106) is the Password Hashing Competition winner, combining the data-dependent memory access of Argon2d with the data-independent pass structure of Argon2i. It is the recommended algorithm when deriving keys from passwords or other low-entropy secrets. This file is the production implementation; `argon2id.native.ts` is the placeholder stub.
|
||||
|
||||
## Features
|
||||
|
||||
- Accepts `Argon2idOptions` with `salt`, `timeCost`, `memoryCost`, `parallelism`, and `length`.
|
||||
- Delegates to `@noble/hashes/argon2` which implements the full RFC 9106 algorithm.
|
||||
- Runs synchronously (no worker/thread offload at this layer).
|
||||
- Exports `Argon2idOptions` for re-export by the selector.
|
||||
- RFC 9106 Appendix B test vectors pass in the test suite.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { argon2id } from "@suaveplan/crypto-hash/primitives/argon2id/argon2id";
|
||||
|
||||
const password = new TextEncoder().encode("correct-horse-battery-staple");
|
||||
const salt = crypto.getRandomValues(new Uint8Array(16));
|
||||
const key = argon2id(password, {
|
||||
salt,
|
||||
timeCost: 3,
|
||||
memoryCost: 65536,
|
||||
parallelism: 4,
|
||||
length: 32,
|
||||
});
|
||||
console.log(key.length); // 32
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { argon2id, type Argon2idOptions } from "@suaveplan/crypto-hash/primitives/argon2id/argon2id";
|
||||
|
||||
// Server-side registration: hash the password and store only the derived key.
|
||||
const SERVER_PARAMS: Omit<Argon2idOptions, "salt"> = {
|
||||
timeCost: 3,
|
||||
memoryCost: 65536,
|
||||
parallelism: 4,
|
||||
length: 64,
|
||||
};
|
||||
|
||||
function hashPassword(password: string): { salt: Uint8Array; key: Uint8Array } {
|
||||
const salt = crypto.getRandomValues(new Uint8Array(16));
|
||||
const key = argon2id(new TextEncoder().encode(password), { ...SERVER_PARAMS, salt });
|
||||
return { salt, key };
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `argon2id(password: Uint8Array, options: Argon2idOptions): Uint8Array`
|
||||
|
||||
Derives key material from a password using Argon2id.
|
||||
|
||||
| Parameter | Type | Description |
|
||||
|-----------|------|-------------|
|
||||
| `password` | `Uint8Array` | Password bytes. |
|
||||
| `options.salt` | `Uint8Array` | Random salt (≥16 bytes per RFC 9106). |
|
||||
| `options.timeCost` | `number` | Number of passes over the memory array (≥1). |
|
||||
| `options.memoryCost` | `number` | Memory usage in KiB (≥8 × parallelism). |
|
||||
| `options.parallelism` | `number` | Degree of parallelism (≥1). |
|
||||
| `options.length` | `number` | Derived key length in bytes (≥4). |
|
||||
|
||||
**Returns:** `Uint8Array` of `options.length` bytes.
|
||||
|
||||
### `Argon2idOptions`
|
||||
|
||||
```ts
|
||||
interface Argon2idOptions {
|
||||
readonly salt: Uint8Array;
|
||||
readonly timeCost: number;
|
||||
readonly memoryCost: number;
|
||||
readonly parallelism: number;
|
||||
readonly length: number;
|
||||
}
|
||||
```
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
Noble maps `timeCost → t`, `memoryCost → m`, `parallelism → p`, and `length → dkLen`. All parameter validation (RFC 9106 minima checks) is delegated to the public KDF façade; this file passes values through without checking. Noble's Argon2id is a pure-JavaScript implementation — no WASM — so it runs in any JavaScript environment but may be slower than a native binary for high memory costs. Synchronous execution on large `memoryCost` values can block the event loop; callers that need async should wrap in a worker thread.
|
||||
@@ -0,0 +1,63 @@
|
||||
# blake2b
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
- [blake2b.noble](./impl/blake2b.noble/blake2b.noble.md) — <!-- begin:auto-docs --> _(no submodules)_- [blake2b](./blake2b.md) — <!-- begin:auto-docs --> _(no submodules)_- blake2b.noble — <!-- begin:auto-docs --> _(no submod…
|
||||
- [blake2b.noble](./impl/blake2b.noble/blake2b.noble.md) — <!-- begin:auto-docs --> _(no submodules)_- [blake2b](./blake2b.md) — <!-- begin:auto-docs --> _(no submodules)_- [blake2b.noble](./impl/blake2b.noble/blake2b.noble.md) — <!-- begin:auto-docs --> _(no submodules)_- [blake2b](./blake2b.md) — <!-- begin:auto-docs --> _(no submodules)_- blake2b.noble — <!-- begin:a…
|
||||
- [blake2b.noble](./impl/blake2b.noble/blake2b.noble.md) — <!-- begin:auto-docs --> _(no submodules)_- [blake2b](./blake2b.md) — <!-- begin:auto-docs --> _(no submodules)_- blake2b.noble — <!-- begin:auto-docs --> _(no submo…
|
||||
- [blake2b.noble](./impl/blake2b.noble/blake2b.noble.md) — <!-- begin:auto-docs --> _(no submodules)_- [blake2b](./blake2b.md) — <!-- begin:auto-docs --> _(no submodules)_
|
||||
## Purpose
|
||||
|
||||
`blake2b.ts` is the public entry point for the BLAKE2b hash primitive (RFC 7693) in `@suaveplan/crypto-hash`. BLAKE2b is a fast, secure cryptographic hash function used by Argon2, password-manager keystreams, blockchain-protocol commitments (Zcash, Polkadot), and many other modern protocols. The suaveplan implementation routes through the per-primitive selector layout (`blake2b.select.ts`) so the noble engine can be replaced without changing caller imports.
|
||||
|
||||
The public surface is two symbols: `blake2b(message, options)` and `Blake2bOptions`. Variable output length 1..64 bytes covers everything from short MACs (e.g. 16-byte authenticator tags) to full 64-byte digests. Optional `key`, `salt`, and `personalization` parameters mix into the BLAKE2b parameter block as specified by RFC 7693.
|
||||
|
||||
## Features
|
||||
|
||||
- RFC 7693 compliance — passes the official `BLAKE2b-512("abc")` known-answer test byte-for-byte.
|
||||
- Variable output length — `dkLen` can be any value in `1..64` (anything outside this range throws `HashError(HASH.BLAKE2B_OUTPUT_TOO_LONG)`).
|
||||
- Optional MAC key — turns the hash into a keyed MAC (BLAKE2b is the only mainstream hash with a built-in MAC mode that does not require an outer HMAC construction). Key length capped at 64 bytes; longer keys throw `HashError(HASH.BLAKE2B_KEY_TOO_LONG)`.
|
||||
- Optional 16-byte `salt` and `personalization` parameters mix into the parameter block (currently passed through to the noble implementation; not all noble versions surface them, so behaviour is "best-effort honored").
|
||||
- Synchronous — `blake2b` returns a `Uint8Array` directly. Cheap enough for the hot path.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { blake2b } from "@suaveplan/crypto-hash/blake2b";
|
||||
|
||||
const digest = blake2b(new TextEncoder().encode("hello world"), { dkLen: 32 });
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { blake2b } from "@suaveplan/crypto-hash/blake2b";
|
||||
|
||||
// Use BLAKE2b in keyed (MAC) mode for short authenticators.
|
||||
function authenticate(message: Uint8Array, secret: Uint8Array): Uint8Array {
|
||||
if (secret.length > 64) {
|
||||
throw new Error("BLAKE2b key must be ≤ 64 bytes");
|
||||
}
|
||||
return blake2b(message, { dkLen: 16, key: secret });
|
||||
}
|
||||
|
||||
// Domain-separated commitments via the personalization parameter (where
|
||||
// the underlying engine surfaces it).
|
||||
function commit(payload: Uint8Array, domain: string): Uint8Array {
|
||||
const personalization = new Uint8Array(16);
|
||||
personalization.set(new TextEncoder().encode(domain.slice(0, 16)));
|
||||
return blake2b(payload, { dkLen: 32, personalization });
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `blake2b(message: Uint8Array, options: Blake2bOptions): Uint8Array` — BLAKE2b hash.
|
||||
- `Blake2bOptions { dkLen: number; key?: Uint8Array; salt?: Uint8Array; personalization?: Uint8Array }` — `dkLen` is the output length in bytes (1..64).
|
||||
|
||||
Errors: `HASH.BLAKE2B_OUTPUT_TOO_LONG` when `dkLen > 64` or `dkLen < 1` or `dkLen` is not an integer; `HASH.BLAKE2B_KEY_TOO_LONG` when `key.length > 64`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The selector pattern (`blake2b.select.ts`) is generated; do not edit it. To add a native engine, place it under `impl/blake2b.native/blake2b.native.ts` and rerun `bun scripts/gen-crypto-selects.ts`. The implementation does not validate that `salt` and `personalization` are exactly 16 bytes — the noble engine ignores incorrectly-sized buffers rather than throwing, and replicating that behaviour here keeps the surface predictable.
|
||||
@@ -0,0 +1,50 @@
|
||||
# blake2b.noble
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
`blake2b.noble.ts` is the noble-backed implementation of BLAKE2b in `@suaveplan/crypto-hash`. It wraps `@noble/hashes/blake2b::blake2b` with parameter validation and the `HashError` substrate. Selector forwarding is done by the generated `blake2b.select.ts` file; consumers should never import from here directly.
|
||||
|
||||
The noble implementation is pure JavaScript and runs everywhere ESM does — Node 20+, Bun, browsers, Deno, Cloudflare Workers. Synchronous; no main-thread yielding needed since BLAKE2b is fast (multi-GiB/s on modern hardware for the 64-byte block size, even in pure JS).
|
||||
|
||||
## Features
|
||||
|
||||
- Wraps `@noble/hashes/blake2b::blake2b` and exposes `blake2b(message, options)`.
|
||||
- Validates `dkLen` in `1..64` integer range; throws `HashError(BLAKE2B_OUTPUT_TOO_LONG)` otherwise.
|
||||
- Validates `key.length <= 64`; throws `HashError(BLAKE2B_KEY_TOO_LONG)` otherwise.
|
||||
- Exposes `Blake2bOptions { dkLen, key?, salt?, personalization? }` as the canonical parameter bundle.
|
||||
- Synchronous (`Uint8Array`).
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { blake2b } from "@suaveplan/crypto-hash/primitives/blake2b/impl/blake2b.noble";
|
||||
|
||||
const out = blake2b(new TextEncoder().encode("hello"), { dkLen: 32 });
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { blake2b } from "@suaveplan/crypto-hash/primitives/blake2b/impl/blake2b.noble";
|
||||
|
||||
// Build a Merkle-leaf commitment scheme using BLAKE2b in keyed mode for
|
||||
// domain-separation. The leaf key is itself derived from a per-tree
|
||||
// commitment key so different trees use different MAC keys.
|
||||
function leaf(treeKey: Uint8Array, payload: Uint8Array): Uint8Array {
|
||||
return blake2b(payload, { dkLen: 32, key: treeKey });
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `blake2b(message: Uint8Array, options: Blake2bOptions): Uint8Array` — pure synchronous BLAKE2b hash.
|
||||
- `Blake2bOptions { dkLen: number; key?: Uint8Array; salt?: Uint8Array; personalization?: Uint8Array }`.
|
||||
|
||||
Errors: `HASH.BLAKE2B_OUTPUT_TOO_LONG`, `HASH.BLAKE2B_KEY_TOO_LONG`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
Validation is checked before delegating to noble so a bad parameter set is rejected before any allocation happens. The noble implementation accepts `salt` and `personalization` parameters but does not strictly require them to be 16 bytes — the suaveplan wrapper passes them through unchanged and lets noble apply its own padding/truncation rules. This matches the upstream library's behaviour and avoids divergent semantics if noble updates its handling in a future release.
|
||||
@@ -0,0 +1,96 @@
|
||||
# hkdf.select
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
`hkdf.select.ts` is the build-time primitive selector for the HKDF implementation in `@suaveplan/crypto-hash`. It re-exports exactly one concrete backend — either `hkdf.noble.ts` (backed by `@noble/hashes`) or `hkdf.native.ts` — depending on the `hkdf` field in `crypto.config.ts`.
|
||||
|
||||
HKDF (HMAC-based Extract-and-Expand Key Derivation Function, RFC 5869) is the standard KDF for deriving multiple independent keys from a single input keying material (IKM) such as a Diffie-Hellman shared secret or a session master secret. This selector exposes the `hkdf` function, `HkdfAlgorithm`, and `HkdfOptions` through a stable interface.
|
||||
|
||||
## Features
|
||||
|
||||
- Re-exports `hkdf(algo, ikm, options)`, `HkdfAlgorithm`, and `HkdfOptions`.
|
||||
- Generated by `bun scripts/gen-crypto-selects.ts` — never hand-edited.
|
||||
- Controlled by `crypto.config.ts`: set `hkdf` to `'noble'` or `'native'`.
|
||||
- Supports SHA-256, SHA-384, and SHA-512 as the underlying PRF.
|
||||
- `HkdfOptions` carries `salt`, `info`, and `length` for Extract and Expand phases.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { hkdf } from "@suaveplan/crypto-hash/primitives/hkdf/hkdf";
|
||||
|
||||
const ikm = new Uint8Array(32).fill(0xff);
|
||||
const key = hkdf("SHA-256", ikm, {
|
||||
salt: new Uint8Array(32),
|
||||
info: new TextEncoder().encode("session-key"),
|
||||
length: 32,
|
||||
});
|
||||
console.log(key.length); // 32
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { hkdf, type HkdfAlgorithm } from "@suaveplan/crypto-hash/primitives/hkdf/hkdf";
|
||||
|
||||
// Derive two independent sub-keys from one shared secret (X25519 output).
|
||||
function deriveKeys(sharedSecret: Uint8Array, salt: Uint8Array): {
|
||||
encKey: Uint8Array;
|
||||
authKey: Uint8Array;
|
||||
} {
|
||||
const enc = new TextEncoder();
|
||||
return {
|
||||
encKey: hkdf("SHA-256", sharedSecret, {
|
||||
salt,
|
||||
info: enc.encode("enc-key"),
|
||||
length: 32,
|
||||
}),
|
||||
authKey: hkdf("SHA-256", sharedSecret, {
|
||||
salt,
|
||||
info: enc.encode("auth-key"),
|
||||
length: 32,
|
||||
}),
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
Re-exports the following from the active implementation:
|
||||
|
||||
### `hkdf(algo: HkdfAlgorithm, ikm: Uint8Array, options: HkdfOptions): Uint8Array`
|
||||
|
||||
Runs HKDF Extract followed by HKDF Expand.
|
||||
|
||||
| Parameter | Type | Description |
|
||||
|-----------|------|-------------|
|
||||
| `algo` | `HkdfAlgorithm` | Hash algorithm for the internal HMAC. |
|
||||
| `ikm` | `Uint8Array` | Input keying material. |
|
||||
| `options.salt` | `Uint8Array` | Optional but strongly recommended random salt. |
|
||||
| `options.info` | `Uint8Array` | Context/application-specific binding string. |
|
||||
| `options.length` | `number` | Output length in bytes (max 255 × hash output length). |
|
||||
|
||||
**Returns:** Derived key material of `options.length` bytes.
|
||||
|
||||
### `HkdfAlgorithm`
|
||||
|
||||
```ts
|
||||
type HkdfAlgorithm = "SHA-256" | "SHA-384" | "SHA-512";
|
||||
```
|
||||
|
||||
### `HkdfOptions`
|
||||
|
||||
```ts
|
||||
interface HkdfOptions {
|
||||
readonly salt: Uint8Array;
|
||||
readonly info: Uint8Array;
|
||||
readonly length: number;
|
||||
}
|
||||
```
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
HKDF is a two-step process. Extract collapses potentially non-uniform IKM into a pseudorandom key (PRK) using HMAC with the salt. Expand stretches PRK into the requested number of output bytes by iterating HMAC with the info string. Noble's `hkdf` function implements RFC 5869 §2 exactly, including the `0x01`, `0x02`, … counter suffixes in the Expand loop. The `length` maximum per RFC 5869 is `255 * HashLen`; violations will cause Noble to throw, which propagates up without wrapping here — validation at the public KDF façade should guard against this before reaching the primitive. The native stub throws `PrimitiveNotImplemented("hkdf")`.
|
||||
@@ -0,0 +1,84 @@
|
||||
# hkdf.native
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
`hkdf.native.ts` is the native-backend placeholder for the HKDF primitive in `@suaveplan/crypto-hash`. It exports the same `hkdf` function, `HkdfAlgorithm` type, and `HkdfOptions` interface as the Noble implementation but throws `PrimitiveNotImplemented("hkdf")` unconditionally.
|
||||
|
||||
This stub keeps the two-implementation selector layout complete for HKDF. It exports all shared types so the selector can re-export them independent of which backend is active. Having the full type surface on both sides means callers can reference `HkdfAlgorithm` and `HkdfOptions` without caring about the selected backend.
|
||||
|
||||
## Features
|
||||
|
||||
- Satisfies `hkdf(algo, ikm, options): Uint8Array` interface.
|
||||
- Exports `HkdfAlgorithm` and `HkdfOptions` (identical to the Noble file).
|
||||
- Throws `PrimitiveNotImplemented("hkdf")` unconditionally.
|
||||
- Zero runtime dependencies beyond `@suaveplan/crypto-utils/errors`.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
// Active only when crypto.config.ts sets hkdf to 'native'.
|
||||
import { hkdf } from "@suaveplan/crypto-hash/primitives/hkdf/hkdf";
|
||||
|
||||
try {
|
||||
hkdf("SHA-256", new Uint8Array(32), {
|
||||
salt: new Uint8Array(32),
|
||||
info: new Uint8Array(0),
|
||||
length: 32,
|
||||
});
|
||||
} catch (err) {
|
||||
// PrimitiveNotImplemented: hkdf
|
||||
console.error(err);
|
||||
}
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
// Use types from the native file to write backend-agnostic caller code.
|
||||
import type { HkdfAlgorithm, HkdfOptions } from "@suaveplan/crypto-hash/primitives/hkdf/hkdf";
|
||||
|
||||
function buildHkdfParams(label: string, algo: HkdfAlgorithm = "SHA-256"): HkdfOptions {
|
||||
return {
|
||||
salt: crypto.getRandomValues(new Uint8Array(32)),
|
||||
info: new TextEncoder().encode(label),
|
||||
length: 32,
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `hkdf(_algo: HkdfAlgorithm, _ikm: Uint8Array, _options: HkdfOptions): Uint8Array`
|
||||
|
||||
Placeholder that always throws.
|
||||
|
||||
| Parameter | Type | Description |
|
||||
|-----------|------|-------------|
|
||||
| `_algo` | `HkdfAlgorithm` | Ignored. |
|
||||
| `_ikm` | `Uint8Array` | Ignored. |
|
||||
| `_options` | `HkdfOptions` | Ignored. |
|
||||
|
||||
**Throws:** `PrimitiveNotImplemented("hkdf")` — always.
|
||||
|
||||
### `HkdfAlgorithm`
|
||||
|
||||
```ts
|
||||
type HkdfAlgorithm = "SHA-256" | "SHA-384" | "SHA-512";
|
||||
```
|
||||
|
||||
### `HkdfOptions`
|
||||
|
||||
```ts
|
||||
interface HkdfOptions {
|
||||
readonly salt: Uint8Array;
|
||||
readonly info: Uint8Array;
|
||||
readonly length: number;
|
||||
}
|
||||
```
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
Types are duplicated in this file rather than imported from the Noble file to keep both implementations self-contained. The selector re-exports from whichever file is active, so callers always get consistent types regardless of the chosen backend. When the in-house HKDF implementation is complete (RFC 5869 KAT suite must pass), the throw is replaced with the real algorithm, parameter prefixes removed, and the selector regenerated.
|
||||
@@ -0,0 +1,90 @@
|
||||
# hkdf.noble
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
`hkdf.noble.ts` is the Noble-backed concrete implementation of HKDF in `@suaveplan/crypto-hash`. It wraps `hkdf` from `@noble/hashes/hkdf`, running both the Extract and Expand phases of RFC 5869 using SHA-256, SHA-384, or SHA-512 as the underlying PRF.
|
||||
|
||||
HKDF transforms input keying material (a Diffie-Hellman shared secret, a session master secret, or any other high-entropy byte sequence) into one or more independent derived keys. This file is the production implementation side of the two-file layout; `hkdf.native.ts` is the placeholder stub.
|
||||
|
||||
## Features
|
||||
|
||||
- Supports `HkdfAlgorithm`: `"SHA-256"`, `"SHA-384"`, `"SHA-512"`.
|
||||
- Runs full RFC 5869 Extract-then-Expand in a single call.
|
||||
- Accepts structured `HkdfOptions` with `salt`, `info`, and `length`.
|
||||
- Returns derived key material of exactly `options.length` bytes.
|
||||
- Exports `HkdfAlgorithm` and `HkdfOptions` for re-export by the selector.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { hkdf } from "@suaveplan/crypto-hash/primitives/hkdf/hkdf";
|
||||
|
||||
const ikm = crypto.getRandomValues(new Uint8Array(32));
|
||||
const key = hkdf("SHA-256", ikm, {
|
||||
salt: new Uint8Array(32),
|
||||
info: new TextEncoder().encode("key"),
|
||||
length: 32,
|
||||
});
|
||||
console.log(key.length); // 32
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { hkdf, type HkdfAlgorithm } from "@suaveplan/crypto-hash/primitives/hkdf/hkdf";
|
||||
|
||||
// Derive three sub-keys from a shared secret for a forward-secure session.
|
||||
function deriveSessionKeys(sharedSecret: Uint8Array, salt: Uint8Array): {
|
||||
encKey: Uint8Array;
|
||||
macKey: Uint8Array;
|
||||
ivBase: Uint8Array;
|
||||
} {
|
||||
const enc = new TextEncoder();
|
||||
const derive = (info: string, len: number) =>
|
||||
hkdf("SHA-256", sharedSecret, { salt, info: enc.encode(info), length: len });
|
||||
return {
|
||||
encKey: derive("enc", 32),
|
||||
macKey: derive("mac", 32),
|
||||
ivBase: derive("iv", 12),
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `hkdf(algo: HkdfAlgorithm, ikm: Uint8Array, options: HkdfOptions): Uint8Array`
|
||||
|
||||
Runs HKDF Extract + Expand.
|
||||
|
||||
| Parameter | Type | Description |
|
||||
|-----------|------|-------------|
|
||||
| `algo` | `HkdfAlgorithm` | Hash algorithm for HMAC-PRF. |
|
||||
| `ikm` | `Uint8Array` | Input keying material (any length). |
|
||||
| `options.salt` | `Uint8Array` | Random salt; use a zero-filled buffer if no salt is available. |
|
||||
| `options.info` | `Uint8Array` | Context/binding string for domain separation. |
|
||||
| `options.length` | `number` | Output length in bytes (max 255 × HashLen). |
|
||||
|
||||
**Returns:** `Uint8Array` of exactly `options.length` bytes.
|
||||
|
||||
### `HkdfAlgorithm`
|
||||
|
||||
```ts
|
||||
type HkdfAlgorithm = "SHA-256" | "SHA-384" | "SHA-512";
|
||||
```
|
||||
|
||||
### `HkdfOptions`
|
||||
|
||||
```ts
|
||||
interface HkdfOptions {
|
||||
readonly salt: Uint8Array;
|
||||
readonly info: Uint8Array;
|
||||
readonly length: number;
|
||||
}
|
||||
```
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The Noble `hkdf(H, ikm, salt, info, dkLen)` call implements RFC 5869 exactly. The internal `hasherFor` helper maps `HkdfAlgorithm` to the Noble hasher via two explicit `if` checks, consistent with the pattern in `hmac.noble.ts` and the SHA-2 dispatch helpers. If `options.length` exceeds `255 × HashLen`, Noble throws a `RangeError`; that error propagates uncaught — the public KDF façade is responsible for validating the length before calling into this primitive. RFC 5869 Appendix A test vectors pass in the test suite.
|
||||
@@ -0,0 +1,76 @@
|
||||
# hmac.select
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
`hmac.select.ts` is the build-time primitive selector for the HMAC implementation within `@suaveplan/crypto-hash`. It re-exports exactly one concrete backend — either `hmac.noble.ts` (backed by `@noble/hashes`) or `hmac.native.ts` — according to the `hmac` entry in `crypto.config.ts`.
|
||||
|
||||
HMAC (Hash-based Message Authentication Code, RFC 2104) is the standard construction for producing a keyed digest from an arbitrary hash function. This selector exposes a single `hmac` function and the `HmacAlgorithm` type, providing a consistent interface regardless of which backend is active.
|
||||
|
||||
## Features
|
||||
|
||||
- Single-symbol re-export of `hmac(algo, key, data)` and `HmacAlgorithm`.
|
||||
- Generated by `bun scripts/gen-crypto-selects.ts` — never hand-edited.
|
||||
- Controlled by `crypto.config.ts`: set `hmac` to `'noble'` or `'native'`.
|
||||
- Supports `"SHA-256"`, `"SHA-384"`, and `"SHA-512"` hash algorithms.
|
||||
- Maintains a stable public API across backend switches.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { hmac } from "@suaveplan/crypto-hash/primitives/hmac/hmac";
|
||||
|
||||
const key = new Uint8Array(32).fill(0xab);
|
||||
const data = new TextEncoder().encode("authenticate me");
|
||||
const tag = hmac("SHA-256", key, data);
|
||||
console.log(tag.length); // 32
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { hmac, type HmacAlgorithm } from "@suaveplan/crypto-hash/primitives/hmac/hmac";
|
||||
|
||||
// Verify a webhook signature from an untrusted header.
|
||||
function verifyWebhookSignature(
|
||||
algo: HmacAlgorithm,
|
||||
secret: Uint8Array,
|
||||
body: Uint8Array,
|
||||
expected: Uint8Array
|
||||
): boolean {
|
||||
const computed = hmac(algo, secret, body);
|
||||
// Constant-time comparison avoids timing side-channels.
|
||||
if (computed.length !== expected.length) return false;
|
||||
let diff = 0;
|
||||
for (let i = 0; i < computed.length; i++) diff |= computed[i] ^ expected[i];
|
||||
return diff === 0;
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
Re-exports the following from the active implementation:
|
||||
|
||||
### `hmac(algo: HmacAlgorithm, key: Uint8Array, data: Uint8Array): Uint8Array`
|
||||
|
||||
Computes HMAC over `data` with `key` using the specified hash algorithm.
|
||||
|
||||
| Parameter | Type | Description |
|
||||
|-----------|------|-------------|
|
||||
| `algo` | `HmacAlgorithm` | `"SHA-256"`, `"SHA-384"`, or `"SHA-512"`. |
|
||||
| `key` | `Uint8Array` | Secret key bytes (any length). |
|
||||
| `data` | `Uint8Array` | Message to authenticate. |
|
||||
|
||||
**Returns:** HMAC tag — 32 bytes for SHA-256, 48 for SHA-384, 64 for SHA-512.
|
||||
|
||||
### `HmacAlgorithm`
|
||||
|
||||
```ts
|
||||
type HmacAlgorithm = "SHA-256" | "SHA-384" | "SHA-512";
|
||||
```
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
HMAC is defined as `H(K XOR opad || H(K XOR ipad || message))`. The Noble backend calls `@noble/hashes/hmac` directly, which implements this construction on top of the same audited SHA-2 primitives used by the standalone hash functions. Key padding and the inner/outer hash passes are handled internally. RFC 2202 (SHA-256) and RFC 4231 (SHA-384/512) test vectors pass in the test suite. The native stub throws `PrimitiveNotImplemented("hmac")` and will be activated in Phase 3 after per-primitive audit.
|
||||
@@ -0,0 +1,73 @@
|
||||
# hmac.native
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
`hmac.native.ts` is the native-backend placeholder for the HMAC primitive in `@suaveplan/crypto-hash`. It exports the same `hmac` function signature and `HmacAlgorithm` type as `hmac.noble.ts`, but throws `PrimitiveNotImplemented("hmac")` unconditionally.
|
||||
|
||||
This stub keeps the two-implementation primitive selector layout complete for HMAC. Both files must exist so the generator script can point the selector at either backend without structural changes. The stub also exports `HmacAlgorithm` so the selector can re-export the type regardless of which backend is active.
|
||||
|
||||
## Features
|
||||
|
||||
- Satisfies `hmac(algo, key, data): Uint8Array` interface.
|
||||
- Exports `HmacAlgorithm = "SHA-256" | "SHA-384" | "SHA-512"` type.
|
||||
- Throws `PrimitiveNotImplemented("hmac")` unconditionally.
|
||||
- Zero runtime dependencies beyond `@suaveplan/crypto-utils/errors`.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
// Active only when crypto.config.ts sets hmac to 'native'.
|
||||
import { hmac } from "@suaveplan/crypto-hash/primitives/hmac/hmac";
|
||||
|
||||
try {
|
||||
hmac("SHA-256", new Uint8Array(32), new Uint8Array(8));
|
||||
} catch (err) {
|
||||
// PrimitiveNotImplemented: hmac
|
||||
console.error(err);
|
||||
}
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
// Capability check pattern for environments where native HMAC may not be available.
|
||||
import { hmac } from "@suaveplan/crypto-hash/primitives/hmac/hmac";
|
||||
|
||||
function isHmacAvailable(): boolean {
|
||||
try {
|
||||
hmac("SHA-256", new Uint8Array(1), new Uint8Array(1));
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `hmac(_algo: HmacAlgorithm, _key: Uint8Array, _data: Uint8Array): Uint8Array`
|
||||
|
||||
Placeholder that always throws.
|
||||
|
||||
| Parameter | Type | Description |
|
||||
|-----------|------|-------------|
|
||||
| `_algo` | `HmacAlgorithm` | Ignored. |
|
||||
| `_key` | `Uint8Array` | Ignored. |
|
||||
| `_data` | `Uint8Array` | Ignored. |
|
||||
|
||||
**Throws:** `PrimitiveNotImplemented("hmac")` — always.
|
||||
|
||||
### `HmacAlgorithm`
|
||||
|
||||
```ts
|
||||
type HmacAlgorithm = "SHA-256" | "SHA-384" | "SHA-512";
|
||||
```
|
||||
|
||||
Exported to allow the selector to re-export the type independent of the chosen backend.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
All three parameters carry underscore prefixes to suppress unused-parameter diagnostics from TypeScript and Biome without inline suppressions. The `HmacAlgorithm` type is duplicated here (rather than imported from the noble file) so this file is self-contained and can be used independently of the noble backend when the selector is pointed at it. When the native implementation is ready, the parameter prefixes are removed, the `throw` is replaced with the real algorithm, and the selector is regenerated.
|
||||
@@ -0,0 +1,78 @@
|
||||
# hmac.noble
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
`hmac.noble.ts` is the Noble-backed concrete implementation of the HMAC primitive in `@suaveplan/crypto-hash`. It wraps `hmac` from `@noble/hashes/hmac`, supporting SHA-256, SHA-384, and SHA-512 as the underlying hash function through the `HmacAlgorithm` discriminant.
|
||||
|
||||
HMAC (RFC 2104) is the standard keyed-hash message authentication code. It proves both message integrity and knowledge of the secret key. This file is the production-ready side of the two-implementation HMAC layout, paired with `hmac.native.ts`.
|
||||
|
||||
## Features
|
||||
|
||||
- Supports `HmacAlgorithm`: `"SHA-256"`, `"SHA-384"`, `"SHA-512"`.
|
||||
- Delegates to `@noble/hashes/hmac` with the appropriate SHA-2 hasher selected at call time.
|
||||
- Returns a fresh tag of 32, 48, or 64 bytes depending on the chosen algorithm.
|
||||
- Exports `HmacAlgorithm` type for use by callers and the selector file.
|
||||
- Zero side effects; thread-safe for concurrent use.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { hmac } from "@suaveplan/crypto-hash/primitives/hmac/hmac";
|
||||
|
||||
const key = crypto.getRandomValues(new Uint8Array(32));
|
||||
const data = new TextEncoder().encode("sign this");
|
||||
const tag = hmac("SHA-256", key, data);
|
||||
console.log(tag.length); // 32
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { hmac, type HmacAlgorithm } from "@suaveplan/crypto-hash/primitives/hmac/hmac";
|
||||
|
||||
// Generic HMAC wrapper that selects strength based on security tier.
|
||||
function computeTag(
|
||||
tier: "standard" | "high",
|
||||
key: Uint8Array,
|
||||
data: Uint8Array
|
||||
): Uint8Array {
|
||||
const algo: HmacAlgorithm = tier === "high" ? "SHA-512" : "SHA-256";
|
||||
return hmac(algo, key, data);
|
||||
}
|
||||
|
||||
const tag = computeTag("high", new Uint8Array(64).fill(1), new Uint8Array(16));
|
||||
console.log(tag.length); // 64
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `hmac(algo: HmacAlgorithm, key: Uint8Array, data: Uint8Array): Uint8Array`
|
||||
|
||||
Computes HMAC over `data` using `key`.
|
||||
|
||||
| Parameter | Type | Description |
|
||||
|-----------|------|-------------|
|
||||
| `algo` | `HmacAlgorithm` | Hash algorithm: `"SHA-256"`, `"SHA-384"`, or `"SHA-512"`. |
|
||||
| `key` | `Uint8Array` | Secret key bytes (any length; Noble pads or hashes internally per RFC 2104). |
|
||||
| `data` | `Uint8Array` | Message bytes to authenticate. |
|
||||
|
||||
**Returns:** HMAC tag — 32 bytes (SHA-256), 48 bytes (SHA-384), or 64 bytes (SHA-512).
|
||||
|
||||
**Throws:** Never under normal conditions.
|
||||
|
||||
### `HmacAlgorithm`
|
||||
|
||||
```ts
|
||||
type HmacAlgorithm = "SHA-256" | "SHA-384" | "SHA-512";
|
||||
```
|
||||
|
||||
### Internal: `hasherFor(algo)`
|
||||
|
||||
A private helper that maps `HmacAlgorithm` to the corresponding Noble hasher class. Not exported.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The `hasherFor` function uses two explicit `if` checks and a fallthrough to SHA-512, matching the pattern used in the other multi-algorithm selectors in this package so that TypeScript's exhaustiveness analysis catches future `HmacAlgorithm` additions. Noble's `hmac(H, key, data)` call handles RFC 2104 key preparation (hashing keys longer than the block size, padding shorter ones with zeros) internally. RFC 2202 and RFC 4231 test vectors pass in the test suite. The tag comparison for verification must use a constant-time equality function from `@suaveplan/crypto-utils` — never a direct byte-array comparison.
|
||||
@@ -0,0 +1,80 @@
|
||||
# pbkdf2.native
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
`pbkdf2.native.ts` is the native-backend placeholder for the PBKDF2 primitive in `@suaveplan/crypto-hash`. It exports the same `pbkdf2` function, `Pbkdf2Algorithm` type, and `Pbkdf2Options` interface as the Noble implementation but throws `PrimitiveNotImplemented("pbkdf2")` unconditionally.
|
||||
|
||||
This file completes the two-implementation selector layout for PBKDF2. Both backends must be present for the generator script and CI layout tests to treat the primitive as fully scaffolded. Exporting the types here makes the selector's re-exports correct regardless of which backend is active.
|
||||
|
||||
## Features
|
||||
|
||||
- Satisfies `pbkdf2(algo, password, options): Uint8Array` interface.
|
||||
- Exports `Pbkdf2Algorithm` and `Pbkdf2Options` (identical to Noble file).
|
||||
- Throws `PrimitiveNotImplemented("pbkdf2")` unconditionally.
|
||||
- Zero runtime dependencies beyond `@suaveplan/crypto-utils/errors`.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
// Active only when crypto.config.ts sets pbkdf2 to 'native'.
|
||||
import { pbkdf2 } from "@suaveplan/crypto-hash/primitives/pbkdf2/pbkdf2";
|
||||
|
||||
try {
|
||||
pbkdf2("SHA-256", new Uint8Array(8), {
|
||||
salt: new Uint8Array(16),
|
||||
iterations: 1,
|
||||
length: 32,
|
||||
});
|
||||
} catch (err) {
|
||||
// PrimitiveNotImplemented: pbkdf2
|
||||
console.error(err);
|
||||
}
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import type { Pbkdf2Options } from "@suaveplan/crypto-hash/primitives/pbkdf2/pbkdf2";
|
||||
|
||||
// Build a Pbkdf2Options object with sensible defaults.
|
||||
function buildParams(salt: Uint8Array): Pbkdf2Options {
|
||||
return { salt, iterations: 600_000, length: 32 };
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `pbkdf2(_algo: Pbkdf2Algorithm, _password: Uint8Array, _options: Pbkdf2Options): Uint8Array`
|
||||
|
||||
Placeholder that always throws.
|
||||
|
||||
| Parameter | Type | Description |
|
||||
|-----------|------|-------------|
|
||||
| `_algo` | `Pbkdf2Algorithm` | Ignored. |
|
||||
| `_password` | `Uint8Array` | Ignored. |
|
||||
| `_options` | `Pbkdf2Options` | Ignored. |
|
||||
|
||||
**Throws:** `PrimitiveNotImplemented("pbkdf2")` — always.
|
||||
|
||||
### `Pbkdf2Algorithm`
|
||||
|
||||
```ts
|
||||
type Pbkdf2Algorithm = "SHA-256" | "SHA-384" | "SHA-512";
|
||||
```
|
||||
|
||||
### `Pbkdf2Options`
|
||||
|
||||
```ts
|
||||
interface Pbkdf2Options {
|
||||
readonly salt: Uint8Array;
|
||||
readonly iterations: number;
|
||||
readonly length: number;
|
||||
}
|
||||
```
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
Types are self-contained here so both implementations are independent of each other. The selector re-exports types from whichever file is active; callers see no difference. When the native PBKDF2 implementation is ready and has passed RFC 6070 / NIST SP 800-132 test vectors via the KAT harness, this file is updated, parameter prefixes removed, and the selector regenerated. Until then, switching `crypto.config.ts` to `'native'` for this primitive will immediately surface the error.
|
||||
@@ -0,0 +1,89 @@
|
||||
# pbkdf2.noble
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
`pbkdf2.noble.ts` is the Noble-backed concrete implementation of PBKDF2 in `@suaveplan/crypto-hash`. It wraps `pbkdf2` from `@noble/hashes/pbkdf2`, supporting SHA-256, SHA-384, and SHA-512 as the underlying HMAC-PRF. The interface accepts a structured `Pbkdf2Options` object carrying the salt, iteration count, and desired output length.
|
||||
|
||||
PBKDF2 (RFC 8018 §5.2) is the oldest of the standard password-based KDFs and enjoys broad native support in WebCrypto, OpenSSL, and PKCS#5 implementations. This file is the production-ready side of the two-implementation layout.
|
||||
|
||||
## Features
|
||||
|
||||
- Supports `Pbkdf2Algorithm`: `"SHA-256"`, `"SHA-384"`, `"SHA-512"`.
|
||||
- Accepts `Pbkdf2Options` with `salt`, `iterations`, and `length`.
|
||||
- Delegates to `@noble/hashes/pbkdf2` without additional abstraction overhead.
|
||||
- Returns derived key material of exactly `options.length` bytes.
|
||||
- Exports `Pbkdf2Algorithm` and `Pbkdf2Options` for the selector.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { pbkdf2 } from "@suaveplan/crypto-hash/primitives/pbkdf2/pbkdf2";
|
||||
|
||||
const password = new TextEncoder().encode("my-password");
|
||||
const salt = crypto.getRandomValues(new Uint8Array(32));
|
||||
const key = pbkdf2("SHA-256", password, {
|
||||
salt,
|
||||
iterations: 600_000,
|
||||
length: 32,
|
||||
});
|
||||
console.log(key.length); // 32
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { pbkdf2, type Pbkdf2Algorithm } from "@suaveplan/crypto-hash/primitives/pbkdf2/pbkdf2";
|
||||
|
||||
// Derive a 48-byte key for AES-384 using SHA-384 PBKDF2.
|
||||
function deriveAesKey(
|
||||
passphrase: string,
|
||||
salt: Uint8Array,
|
||||
algo: Pbkdf2Algorithm = "SHA-384",
|
||||
iterations = 310_000
|
||||
): Uint8Array {
|
||||
return pbkdf2(algo, new TextEncoder().encode(passphrase), {
|
||||
salt,
|
||||
iterations,
|
||||
length: 48,
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `pbkdf2(algo: Pbkdf2Algorithm, password: Uint8Array, options: Pbkdf2Options): Uint8Array`
|
||||
|
||||
Derives key material from a password.
|
||||
|
||||
| Parameter | Type | Description |
|
||||
|-----------|------|-------------|
|
||||
| `algo` | `Pbkdf2Algorithm` | Hash algorithm for the internal HMAC-PRF. |
|
||||
| `password` | `Uint8Array` | Password bytes. |
|
||||
| `options.salt` | `Uint8Array` | Random salt (≥16 bytes recommended by SP 800-132). |
|
||||
| `options.iterations` | `number` | Iteration count (OWASP: ≥600 000 for SHA-256). |
|
||||
| `options.length` | `number` | Derived key length in bytes. |
|
||||
|
||||
**Returns:** `Uint8Array` of `options.length` bytes.
|
||||
|
||||
### `Pbkdf2Algorithm`
|
||||
|
||||
```ts
|
||||
type Pbkdf2Algorithm = "SHA-256" | "SHA-384" | "SHA-512";
|
||||
```
|
||||
|
||||
### `Pbkdf2Options`
|
||||
|
||||
```ts
|
||||
interface Pbkdf2Options {
|
||||
readonly salt: Uint8Array;
|
||||
readonly iterations: number;
|
||||
readonly length: number;
|
||||
}
|
||||
```
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
Noble's `pbkdf2(H, password, salt, { c, dkLen })` maps directly to RFC 8018 PBKDF2. The `iterations` field in `Pbkdf2Options` maps to Noble's `c` (count) parameter, and `length` maps to `dkLen`. Parameter validation (iteration floor, length bounds) is the responsibility of the public KDF façade; this file passes values through without checking. The `hasherFor` helper uses the same two-`if` pattern as the HMAC and HKDF implementations for TypeScript exhaustiveness. NIST SP 800-132 and RFC 6070 test vectors pass in the test suite.
|
||||
@@ -0,0 +1,91 @@
|
||||
# pbkdf2.select
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
`pbkdf2.select.ts` is the build-time primitive selector for the PBKDF2 implementation in `@suaveplan/crypto-hash`. It re-exports exactly one concrete backend — either `pbkdf2.noble.ts` or `pbkdf2.native.ts` — based on the `pbkdf2` entry in `crypto.config.ts`.
|
||||
|
||||
PBKDF2 (Password-Based Key Derivation Function 2, RFC 8018) is a widely supported standard for key derivation from passwords. While slower than Argon2id for pure password hashing, it is available natively in WebCrypto and is commonly required by interoperability profiles (PKCS#5, FIPS 140). The selector exposes `pbkdf2`, `Pbkdf2Algorithm`, and `Pbkdf2Options` through a stable interface.
|
||||
|
||||
## Features
|
||||
|
||||
- Re-exports `pbkdf2(algo, password, options)`, `Pbkdf2Algorithm`, and `Pbkdf2Options`.
|
||||
- Generated by `bun scripts/gen-crypto-selects.ts` — never hand-edited.
|
||||
- Controlled by `crypto.config.ts`: set `pbkdf2` to `'noble'` or `'native'`.
|
||||
- Supports SHA-256, SHA-384, and SHA-512 as the underlying PRF.
|
||||
- Parameter validation (OWASP iteration minimums) is enforced at the public façade, not here.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { pbkdf2 } from "@suaveplan/crypto-hash/primitives/pbkdf2/pbkdf2";
|
||||
|
||||
const password = new TextEncoder().encode("hunter2");
|
||||
const salt = crypto.getRandomValues(new Uint8Array(32));
|
||||
const key = pbkdf2("SHA-256", password, {
|
||||
salt,
|
||||
iterations: 600_000,
|
||||
length: 32,
|
||||
});
|
||||
console.log(key.length); // 32
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { pbkdf2, type Pbkdf2Algorithm } from "@suaveplan/crypto-hash/primitives/pbkdf2/pbkdf2";
|
||||
|
||||
// Derive an AES-256 key from a passphrase with a stored salt.
|
||||
function deriveStorageKey(
|
||||
passphrase: string,
|
||||
storedSalt: Uint8Array,
|
||||
algo: Pbkdf2Algorithm = "SHA-256",
|
||||
iterations: number = 600_000
|
||||
): Uint8Array {
|
||||
return pbkdf2(algo, new TextEncoder().encode(passphrase), {
|
||||
salt: storedSalt,
|
||||
iterations,
|
||||
length: 32,
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
Re-exports the following from the active implementation:
|
||||
|
||||
### `pbkdf2(algo: Pbkdf2Algorithm, password: Uint8Array, options: Pbkdf2Options): Uint8Array`
|
||||
|
||||
Derives a key from a password using PBKDF2.
|
||||
|
||||
| Parameter | Type | Description |
|
||||
|-----------|------|-------------|
|
||||
| `algo` | `Pbkdf2Algorithm` | Hash algorithm for the internal HMAC-PRF. |
|
||||
| `password` | `Uint8Array` | Password bytes. |
|
||||
| `options.salt` | `Uint8Array` | Cryptographically random salt (≥16 bytes recommended). |
|
||||
| `options.iterations` | `number` | Iteration count; OWASP recommends ≥600 000 for SHA-256. |
|
||||
| `options.length` | `number` | Derived key length in bytes. |
|
||||
|
||||
**Returns:** Derived key of `options.length` bytes.
|
||||
|
||||
### `Pbkdf2Algorithm`
|
||||
|
||||
```ts
|
||||
type Pbkdf2Algorithm = "SHA-256" | "SHA-384" | "SHA-512";
|
||||
```
|
||||
|
||||
### `Pbkdf2Options`
|
||||
|
||||
```ts
|
||||
interface Pbkdf2Options {
|
||||
readonly salt: Uint8Array;
|
||||
readonly iterations: number;
|
||||
readonly length: number;
|
||||
}
|
||||
```
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
PBKDF2 applies HMAC repeatedly (`iterations` times) to produce the derived key material. The Noble implementation passes NIST SP 800-132 and RFC 6070 test vectors. The iteration count has significant impact on compute time: 600 000 SHA-256 iterations take roughly 200–500 ms on a commodity CPU. Callers that need stronger memory-hardness should use Argon2id instead. The primitive does not enforce a minimum iteration count — that check belongs at the public KDF façade layer. The native stub throws `PrimitiveNotImplemented("pbkdf2")`.
|
||||
@@ -0,0 +1,62 @@
|
||||
# scrypt.noble
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
`scrypt.noble.ts` is the noble-backed implementation of the scrypt KDF in `@suaveplan/crypto-hash`. It wraps `@noble/hashes/scrypt::scryptAsync` with parameter validation and the `HashError` substrate. Selector forwarding is done by the generated `scrypt.select.ts` file; consumers should never import from here directly.
|
||||
|
||||
The noble implementation is pure JavaScript, async-friendly, and has zero dependencies on Node.js APIs — making it safe in browsers, Cloudflare Workers, Deno, Bun, and Node. The async design lets the runtime yield on long-running parameter sets so the main thread does not block.
|
||||
|
||||
## Features
|
||||
|
||||
- Wraps `@noble/hashes/scrypt::scryptAsync` and exposes `scrypt(password, salt, options)`.
|
||||
- Validates `N` is a power of two ≥ 2; throws `HashError(SCRYPT_INVALID_N)` otherwise.
|
||||
- Exposes `ScryptOptions { N, r, p, dkLen }` as the canonical parameter bundle.
|
||||
- Pure async (`Promise<Uint8Array>`).
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { scrypt } from "@suaveplan/crypto-hash/primitives/scrypt/impl/scrypt.noble";
|
||||
|
||||
const out = await scrypt(
|
||||
new TextEncoder().encode("password"),
|
||||
new TextEncoder().encode("salt"),
|
||||
{ N: 16_384, r: 8, p: 1, dkLen: 32 }
|
||||
);
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { scrypt } from "@suaveplan/crypto-hash/primitives/scrypt/impl/scrypt.noble";
|
||||
|
||||
// Probe the parameter cost for adaptive tuning — measure how long
|
||||
// `(N=2^k, r=8, p=1, dkLen=32)` takes for k in 14..20 and pick the
|
||||
// largest k that completes under the configured target (e.g. 250ms).
|
||||
async function calibrate(targetMs: number): Promise<number> {
|
||||
for (let k = 14; k <= 20; k++) {
|
||||
const start = performance.now();
|
||||
await scrypt(
|
||||
new TextEncoder().encode("probe"),
|
||||
new TextEncoder().encode("probe"),
|
||||
{ N: 2 ** k, r: 8, p: 1, dkLen: 32 }
|
||||
);
|
||||
if (performance.now() - start > targetMs) return k - 1;
|
||||
}
|
||||
return 20;
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `scrypt(password: Uint8Array, salt: Uint8Array, options: ScryptOptions): Promise<Uint8Array>` — RFC 7914 scrypt KDF.
|
||||
- `ScryptOptions { N: number; r: number; p: number; dkLen: number }` — `N` is the CPU/memory cost (must be a power of two ≥ 2), `r` is block size, `p` is parallelisation, `dkLen` is output length in bytes.
|
||||
|
||||
Errors: `HASH.SCRYPT_INVALID_N` for `N` that is not a power of two ≥ 2.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
`isPowerOfTwo(n)` uses the standard bitwise trick `n > 0 && (n & (n - 1)) === 0`. Validation runs before any work so a wrong `N` does not allocate the inner state buffer. The package keeps `r` and `p` unconstrained — RFC 7914 does not impose hard limits and tightening here would diverge from upstream interoperability. Callers with stricter policy needs (e.g. OWASP-recommended minimums) should layer that on top of the primitive, not inside it.
|
||||
@@ -0,0 +1,71 @@
|
||||
# scrypt
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
- [scrypt.noble](./impl/scrypt.noble/scrypt.noble.md) — <!-- begin:auto-docs --> _(no submodules)_- [scrypt](./scrypt.md) — <!-- begin:auto-docs --> _(no submodules)_- scrypt.noble — <!-- begin:auto-docs --> _(no submodu…
|
||||
- [scrypt.noble](./impl/scrypt.noble/scrypt.noble.md) — <!-- begin:auto-docs --> _(no submodules)_- [scrypt](./scrypt.md) — <!-- begin:auto-docs --> _(no submodules)_- [scrypt.noble](./impl/scrypt.noble/scrypt.noble.md) — <!-- begin:auto-docs --> _(no submodules)_- [scrypt](./scrypt.md) — <!-- begin:auto-docs --> _(no submodules)_- scrypt.noble — <!-- begin:au…
|
||||
- [scrypt.noble](./impl/scrypt.noble/scrypt.noble.md) — <!-- begin:auto-docs --> _(no submodules)_- [scrypt](./scrypt.md) — <!-- begin:auto-docs --> _(no submodules)_- scrypt.noble — <!-- begin:auto-docs --> _(no submod…
|
||||
- [scrypt.noble](./impl/scrypt.noble/scrypt.noble.md) — <!-- begin:auto-docs --> _(no submodules)_- [scrypt](./scrypt.md) — <!-- begin:auto-docs --> _(no submodules)_
|
||||
## Purpose
|
||||
|
||||
`scrypt.ts` is the public entry point for the scrypt KDF (RFC 7914) primitive in `@suaveplan/crypto-hash`. scrypt is a memory-hard password-based key derivation function — its `N` parameter forces an attacker to allocate `N * r * 64` bytes of fast memory per guess, which makes brute-force attacks on hardware accelerators dramatically more expensive than they would be against PBKDF2 or even bcrypt. It is the right KDF when callers want stronger memory-hardness than PBKDF2 but cannot adopt Argon2id (e.g. cross-stack interop with older Bitcoin / Litecoin tooling, or compatibility with the standard library on platforms that ship scrypt natively).
|
||||
|
||||
The suaveplan implementation routes through the per-primitive selector layout (`scrypt.select.ts`) so the `noble` engine can be replaced by a `native` engine (libsodium-style) without changing any caller imports. The public surface is two symbols — `scrypt` and `ScryptOptions`.
|
||||
|
||||
## Features
|
||||
|
||||
- RFC 7914 compliance — passes the official `(P="password", S="NaCl", N=1024, r=8, p=16, dkLen=64)` known-answer test byte-for-byte.
|
||||
- Async API (`Promise<Uint8Array>`) so the noble implementation can yield to the event loop on long-running parameter sets without blocking the main thread.
|
||||
- Strict parameter validation: `N` must be a power of two ≥ 2; otherwise throws `HashError(HASH.SCRYPT_INVALID_N)` with the offending value in `context.N`.
|
||||
- Variable output length via `dkLen`. No upper bound is enforced beyond the underlying noble implementation's limits.
|
||||
- Caller-supplied salt — the primitive never generates randomness internally.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { scrypt } from "@suaveplan/crypto-hash/scrypt";
|
||||
|
||||
const salt = crypto.getRandomValues(new Uint8Array(16));
|
||||
const derived = await scrypt(
|
||||
new TextEncoder().encode("correct horse battery staple"),
|
||||
salt,
|
||||
{ N: 16_384, r: 8, p: 1, dkLen: 32 }
|
||||
);
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { scrypt } from "@suaveplan/crypto-hash/scrypt";
|
||||
|
||||
// Migrating a legacy verifier table: re-derive each row at the stored
|
||||
// `(N, r, p, salt)` tuple and compare against the stored hash.
|
||||
async function verifyLegacy(
|
||||
password: string,
|
||||
row: { salt: Uint8Array; N: number; r: number; p: number; dkLen: number; expected: Uint8Array }
|
||||
): Promise<boolean> {
|
||||
const candidate = await scrypt(
|
||||
new TextEncoder().encode(password),
|
||||
row.salt,
|
||||
{ N: row.N, r: row.r, p: row.p, dkLen: row.dkLen }
|
||||
);
|
||||
if (candidate.length !== row.expected.length) return false;
|
||||
let diff = 0;
|
||||
for (let i = 0; i < candidate.length; i++) {
|
||||
diff |= candidate[i]! ^ row.expected[i]!;
|
||||
}
|
||||
return diff === 0;
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `scrypt(password: Uint8Array, salt: Uint8Array, options: ScryptOptions): Promise<Uint8Array>` — derives `dkLen` bytes via RFC 7914 scrypt.
|
||||
- `ScryptOptions { N: number; r: number; p: number; dkLen: number }` — `N` must be a power of two ≥ 2; otherwise `HashError(HASH.SCRYPT_INVALID_N)` is thrown.
|
||||
|
||||
Errors: `HASH.SCRYPT_INVALID_N`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The noble engine is async because scrypt's inner loop ("ROMix") is CPU-bound; making it async lets the runtime opportunistically yield. The suaveplan implementation does not bound `N` from above — callers may use `N=2^20` (~1 GiB peak memory at `r=8`) if they want to. Callers in performance-sensitive paths should pre-warm the work pool, since the first invocation per worker thread typically takes 2–3× longer than steady-state due to V8/JIT profiling. The selector pattern (`scrypt.select.ts`) is generated; do not edit it. To add a native engine, place it under `impl/scrypt.native/scrypt.native.ts` and rerun `bun scripts/gen-crypto-selects.ts`.
|
||||
@@ -0,0 +1,65 @@
|
||||
# sha256.native
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
`sha256.native.ts` is the native-backend placeholder for the SHA-256 primitive in `@suaveplan/crypto-hash`. It satisfies the same interface as `sha256.noble.ts` — accepting a `Uint8Array` and returning a `Uint8Array` — but throws `PrimitiveNotImplemented("sha256")` unconditionally.
|
||||
|
||||
This stub exists to keep the two-implementation primitive selector layout complete so that `crypto.config.ts` can be switched to `'native'` once an in-house SHA-256 implementation is ready. The generator script `gen-crypto-selects.ts` can point the selector at this file as soon as the implementation is wired up and passes the KAT harness in `@suaveplan/crypto-utils`.
|
||||
|
||||
## Features
|
||||
|
||||
- Satisfies the `sha256(data: Uint8Array): Uint8Array` interface required by the selector.
|
||||
- Throws `PrimitiveNotImplemented("sha256")` from `@suaveplan/crypto-utils/errors`.
|
||||
- Signals clearly that the native path is not yet active, preventing silent misuse.
|
||||
- Zero runtime dependencies beyond `@suaveplan/crypto-utils`.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
// This file is only active when crypto.config.ts sets sha256 to 'native'.
|
||||
// Currently throws — the example shows what the call site looks like.
|
||||
import { sha256 } from "@suaveplan/crypto-hash/primitives/sha256/sha256";
|
||||
|
||||
try {
|
||||
const digest = sha256(new Uint8Array(32));
|
||||
} catch (err) {
|
||||
// PrimitiveNotImplemented: sha256
|
||||
console.error(err);
|
||||
}
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
// During Phase 2 development, replace the throw with a real implementation.
|
||||
// The signature must remain: (data: Uint8Array): Uint8Array
|
||||
import { sha256 } from "@suaveplan/crypto-hash/primitives/sha256/sha256";
|
||||
|
||||
// Wrap with a capability check to gracefully degrade when native is unavailable.
|
||||
function tryNativeSha256(data: Uint8Array): Uint8Array | null {
|
||||
try {
|
||||
return sha256(data);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `sha256(_data: Uint8Array): Uint8Array`
|
||||
|
||||
Placeholder that always throws.
|
||||
|
||||
| Parameter | Type | Description |
|
||||
|-----------|------|-------------|
|
||||
| `_data` | `Uint8Array` | Ignored. |
|
||||
|
||||
**Throws:** `PrimitiveNotImplemented("sha256")` — always.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The `_data` parameter prefix signals to TypeScript and Biome that the argument is intentionally unused. The thrown `PrimitiveNotImplemented` error is defined in `@suaveplan/crypto-utils/errors` and extends `SuaveplanError` with a code that identifies which primitive is missing. When the native implementation is ready, this file's body is replaced with the real algorithm and the prefix removed from the parameter name; the selector file is then regenerated to point at this file. No other files change during that switch.
|
||||
@@ -0,0 +1,60 @@
|
||||
# sha256.noble
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
`sha256.noble.ts` is the Noble-backed concrete implementation of the SHA-256 primitive in `@suaveplan/crypto-hash`. It wraps `sha256` from `@noble/hashes/sha2`, exposing the same one-shot signature (`Uint8Array → Uint8Array`) that the selector and public façade expect.
|
||||
|
||||
This file is one side of the two-implementation layout: `sha256.noble.ts` (production-ready, audited) and `sha256.native.ts` (placeholder stub). The selector file `sha256.select.ts` chooses between them at build time; callers never import this file directly.
|
||||
|
||||
## Features
|
||||
|
||||
- Delegates entirely to `@noble/hashes/sha2`: no custom SHA-2 arithmetic.
|
||||
- Returns a fresh 32-byte `Uint8Array` on every call.
|
||||
- Zero side effects: no global state, no lazy initialisation.
|
||||
- Tree-shakeable: when the selector chooses the native backend, this file is unreachable and eliminated by the bundler.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
// Import via the stable public entry point, not this impl file directly.
|
||||
import { sha256 } from "@suaveplan/crypto-hash/primitives/sha256/sha256";
|
||||
|
||||
const digest = sha256(new TextEncoder().encode("hello world"));
|
||||
console.log(digest.length); // 32
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { sha256 } from "@suaveplan/crypto-hash/primitives/sha256/sha256";
|
||||
import { toHex } from "@suaveplan/crypto-hash/encoding";
|
||||
|
||||
// Hash-then-sign pattern: hash the message before passing to an ECDSA signer.
|
||||
function preHash(message: Uint8Array): string {
|
||||
return toHex(sha256(message));
|
||||
}
|
||||
|
||||
const hex = preHash(new TextEncoder().encode("sign me"));
|
||||
console.log(hex.length); // 64 hex chars
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `sha256(data: Uint8Array): Uint8Array`
|
||||
|
||||
Computes the SHA-256 digest of `data`.
|
||||
|
||||
| Parameter | Type | Description |
|
||||
|-----------|------|-------------|
|
||||
| `data` | `Uint8Array` | Arbitrary byte sequence. |
|
||||
|
||||
**Returns:** Fresh 32-byte `Uint8Array`.
|
||||
|
||||
**Throws:** Never — Noble's SHA-256 accepts any `Uint8Array` input without error.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The function is a single-expression wrapper: `return nobleSha256(data)`. Noble allocates a new output buffer internally on every call, so the returned array is always independent of the input. There is no incremental/streaming interface at this layer; callers that need to hash large inputs in chunks should concatenate first or use a higher-level streaming abstraction. This implementation has been audited by the Noble maintainers and passes NIST FIPS 180-4 / CAVP test vectors.
|
||||
@@ -0,0 +1,71 @@
|
||||
# sha256.select
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
`sha256.select.ts` is the build-time primitive selector for the SHA-256 implementation. It is a generated file that re-exports exactly one concrete implementation — either `sha256.noble.ts` (the audited `@noble/hashes` backend) or `sha256.native.ts` (the in-house native backend) — based on the setting in `crypto.config.ts`.
|
||||
|
||||
Consumers of `@suaveplan/crypto-hash` import SHA-256 through this file (via the public entry point) and never reference a concrete implementation directly. Swapping backends requires only regenerating this file; no import changes propagate to callers.
|
||||
|
||||
## Features
|
||||
|
||||
- Single-symbol re-export of `sha256(data: Uint8Array): Uint8Array`.
|
||||
- Generated by `bun scripts/gen-crypto-selects.ts` — never hand-edited.
|
||||
- Controlled by `crypto.config.ts` at the package root: set `sha256` to `'noble'` or `'native'`.
|
||||
- Keeps the public API stable across backend switches.
|
||||
- Enables dead-code elimination: tree-shakers see a single concrete import, not a runtime branch.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
// Import via the public package entry point — never import the select file directly.
|
||||
import { sha256 } from "@suaveplan/crypto-hash/primitives/sha256/sha256";
|
||||
|
||||
const digest = sha256(new TextEncoder().encode("hello"));
|
||||
console.log(digest.length); // 32
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { sha256 } from "@suaveplan/crypto-hash/primitives/sha256/sha256";
|
||||
|
||||
// Hash a concatenation of chunks without an intermediate allocation.
|
||||
function hashChunks(chunks: Uint8Array[]): Uint8Array {
|
||||
let totalLength = 0;
|
||||
for (const c of chunks) totalLength += c.length;
|
||||
const combined = new Uint8Array(totalLength);
|
||||
let offset = 0;
|
||||
for (const c of chunks) {
|
||||
combined.set(c, offset);
|
||||
offset += c.length;
|
||||
}
|
||||
return sha256(combined);
|
||||
}
|
||||
|
||||
const digest = hashChunks([
|
||||
new Uint8Array([0x01, 0x02]),
|
||||
new Uint8Array([0x03, 0x04]),
|
||||
]);
|
||||
console.log(digest.length); // 32
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
Re-exports the following from the active implementation:
|
||||
|
||||
### `sha256(data: Uint8Array): Uint8Array`
|
||||
|
||||
One-shot SHA-256 digest.
|
||||
|
||||
| Parameter | Type | Description |
|
||||
|-----------|------|-------------|
|
||||
| `data` | `Uint8Array` | Arbitrary byte sequence to hash. |
|
||||
|
||||
**Returns:** Fresh 32-byte `Uint8Array` containing the SHA-256 digest.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
This file is marked `/* GENERATED — DO NOT EDIT */`. The generator script reads `crypto.config.ts`, resolves the implementation path for `sha256`, and writes a single `export * from` line. At present the default configuration selects the Noble backend because the native implementation is a placeholder that throws `PrimitiveNotImplemented`. The native backend will be activated once the in-house SHA-256 implementation passes the known-answer-test (KAT) harness in `@suaveplan/crypto-utils`.
|
||||
@@ -0,0 +1,64 @@
|
||||
# sha384.native
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
`sha384.native.ts` is the native-backend placeholder for the SHA-384 primitive in `@suaveplan/crypto-hash`. It exposes the same `sha384(data: Uint8Array): Uint8Array` signature as the Noble implementation but throws `PrimitiveNotImplemented("sha384")` unconditionally.
|
||||
|
||||
The file exists to keep the two-implementation primitive selector layout structurally complete for all primitives in the package. Having both `sha384.noble.ts` and `sha384.native.ts` present means the generator script can switch backends without any missing-file errors, and CI can validate the native stub's presence as part of the layout test.
|
||||
|
||||
## Features
|
||||
|
||||
- Satisfies `sha384(data: Uint8Array): Uint8Array` interface for the selector.
|
||||
- Throws `PrimitiveNotImplemented("sha384")` unconditionally.
|
||||
- Signals clearly that this path is not yet active.
|
||||
- Zero runtime dependencies beyond `@suaveplan/crypto-utils/errors`.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
// Active only when crypto.config.ts sets sha384 to 'native'.
|
||||
// Currently throws unconditionally.
|
||||
import { sha384 } from "@suaveplan/crypto-hash/primitives/sha384/sha384";
|
||||
|
||||
try {
|
||||
sha384(new Uint8Array(32));
|
||||
} catch (err) {
|
||||
// PrimitiveNotImplemented: sha384
|
||||
console.error(err);
|
||||
}
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
// Pattern for graceful capability detection during native migration.
|
||||
import { sha384 } from "@suaveplan/crypto-hash/primitives/sha384/sha384";
|
||||
|
||||
function isSha384Available(): boolean {
|
||||
try {
|
||||
sha384(new Uint8Array(1));
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `sha384(_data: Uint8Array): Uint8Array`
|
||||
|
||||
Placeholder that always throws.
|
||||
|
||||
| Parameter | Type | Description |
|
||||
|-----------|------|-------------|
|
||||
| `_data` | `Uint8Array` | Ignored. |
|
||||
|
||||
**Throws:** `PrimitiveNotImplemented("sha384")` — always.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
Like all native stubs in the package, the underscore prefix on `_data` suppresses unused-parameter warnings from TypeScript and Biome without requiring a lint-disable comment. When the in-house SHA-384 implementation is complete and has passed the NIST CAVP test suite (via the KAT harness in `@suaveplan/crypto-utils`), this file is updated with the real implementation and the selector is regenerated to point here.
|
||||
@@ -0,0 +1,59 @@
|
||||
# sha384.noble
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
`sha384.noble.ts` is the Noble-backed concrete implementation of the SHA-384 primitive in `@suaveplan/crypto-hash`. It delegates to `sha384` from `@noble/hashes/sha2`, exposing a minimal one-shot interface that the selector and public façade expect.
|
||||
|
||||
SHA-384 is the FIPS 180-4 truncation of SHA-512 to 48 bytes. It is preferred in high-throughput 64-bit environments over SHA-256 because SHA-512's 64-bit word arithmetic is natively fast on modern hardware, and the truncation gives a slight output-size reduction. This file is the production-quality side of the two-implementation layout.
|
||||
|
||||
## Features
|
||||
|
||||
- Delegates entirely to `@noble/hashes/sha2` SHA-384 implementation.
|
||||
- Returns a fresh 48-byte `Uint8Array` on every call.
|
||||
- Zero side effects; no global state or lazy initialisation.
|
||||
- Tree-shakeable: unreachable when the native backend is selected.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { sha384 } from "@suaveplan/crypto-hash/primitives/sha384/sha384";
|
||||
|
||||
const digest = sha384(new TextEncoder().encode("hello"));
|
||||
console.log(digest.length); // 48
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { sha384 } from "@suaveplan/crypto-hash/primitives/sha384/sha384";
|
||||
import { toHex } from "@suaveplan/crypto-hash/encoding";
|
||||
|
||||
// Compute a SHA-384 content hash for cache busting.
|
||||
function contentHash(payload: Uint8Array): string {
|
||||
return `sha384-${toHex(sha384(payload))}`;
|
||||
}
|
||||
|
||||
const tag = contentHash(new TextEncoder().encode("bundle contents"));
|
||||
console.log(tag.startsWith("sha384-")); // true
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `sha384(data: Uint8Array): Uint8Array`
|
||||
|
||||
Computes the SHA-384 digest of `data`.
|
||||
|
||||
| Parameter | Type | Description |
|
||||
|-----------|------|-------------|
|
||||
| `data` | `Uint8Array` | Arbitrary byte sequence. |
|
||||
|
||||
**Returns:** Fresh 48-byte `Uint8Array`.
|
||||
|
||||
**Throws:** Never.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The function body is `return nobleSha384(data)` — a single pass-through with no added logic. Noble allocates the output buffer internally. SHA-384 shares the SHA-512 compression function and differs only in the initial hash values and the truncation of the final state to 384 bits. The Noble implementation passes NIST CAVP vectors for SHA-384. No streaming interface is exposed at this layer.
|
||||
@@ -0,0 +1,60 @@
|
||||
# sha384.select
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
`sha384.select.ts` is the build-time primitive selector for the SHA-384 implementation. It is a generated file that re-exports exactly one concrete implementation — either `sha384.noble.ts` (the audited `@noble/hashes` backend) or `sha384.native.ts` (the in-house native backend) — based on the setting in `crypto.config.ts`.
|
||||
|
||||
Consumers import SHA-384 through the stable public entry point, which resolves here; they are insulated from backend changes. Regenerating this file via `bun scripts/gen-crypto-selects.ts` is the only step required to swap implementations.
|
||||
|
||||
## Features
|
||||
|
||||
- Single-symbol re-export of `sha384(data: Uint8Array): Uint8Array`.
|
||||
- Generated by `bun scripts/gen-crypto-selects.ts` — never hand-edited.
|
||||
- Controlled by `crypto.config.ts`: set `sha384` to `'noble'` or `'native'`.
|
||||
- Maintains a stable public API across backend switches.
|
||||
- Enables static dead-code elimination of the unused backend.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { sha384 } from "@suaveplan/crypto-hash/primitives/sha384/sha384";
|
||||
|
||||
const digest = sha384(new TextEncoder().encode("hello"));
|
||||
console.log(digest.length); // 48
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { sha384 } from "@suaveplan/crypto-hash/primitives/sha384/sha384";
|
||||
|
||||
// Compute a commitment to a serialised record.
|
||||
function commit(record: Record<string, string>): Uint8Array {
|
||||
const encoded = new TextEncoder().encode(JSON.stringify(record));
|
||||
return sha384(encoded);
|
||||
}
|
||||
|
||||
const hash = commit({ userId: "u_123", action: "login" });
|
||||
console.log(hash.length); // 48
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
Re-exports the following from the active implementation:
|
||||
|
||||
### `sha384(data: Uint8Array): Uint8Array`
|
||||
|
||||
One-shot SHA-384 digest.
|
||||
|
||||
| Parameter | Type | Description |
|
||||
|-----------|------|-------------|
|
||||
| `data` | `Uint8Array` | Arbitrary byte sequence to hash. |
|
||||
|
||||
**Returns:** Fresh 48-byte `Uint8Array` containing the SHA-384 digest.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
SHA-384 is a truncation of SHA-512 that shares the same 64-bit word operations and block size. It is preferred over SHA-256 in environments that process large amounts of data on 64-bit processors because the 64-bit arithmetic is natively fast, and the truncation provides a modest reduction in output size relative to SHA-512. The Noble backend delegates to `@noble/hashes/sha2` without any added overhead. The native stub throws `PrimitiveNotImplemented("sha384")` until the in-house implementation is ready and has passed the NIST CAVP test vectors.
|
||||
@@ -0,0 +1,68 @@
|
||||
# sha512.native
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
`sha512.native.ts` is the native-backend placeholder for the SHA-512 primitive in `@suaveplan/crypto-hash`. It satisfies the `sha512(data: Uint8Array): Uint8Array` interface but throws `PrimitiveNotImplemented("sha512")` unconditionally until an in-house implementation is ready.
|
||||
|
||||
This file pairs with `sha512.noble.ts` to complete the two-implementation selector layout. Its presence ensures the build system, generator script, and CI layout tests can treat SHA-512 consistently with every other primitive in the package.
|
||||
|
||||
## Features
|
||||
|
||||
- Satisfies the `sha512(data: Uint8Array): Uint8Array` interface.
|
||||
- Throws `PrimitiveNotImplemented("sha512")` unconditionally.
|
||||
- Provides a clear, typed error that identifies the missing primitive by name.
|
||||
- Zero runtime dependencies beyond `@suaveplan/crypto-utils/errors`.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
// Active only when crypto.config.ts sets sha512 to 'native'.
|
||||
import { sha512 } from "@suaveplan/crypto-hash/primitives/sha512/sha512";
|
||||
|
||||
try {
|
||||
sha512(new Uint8Array(64));
|
||||
} catch (err) {
|
||||
// PrimitiveNotImplemented: sha512
|
||||
console.error(err);
|
||||
}
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
// Feature detection for environments that may or may not have native SHA-512.
|
||||
import { sha512 } from "@suaveplan/crypto-hash/primitives/sha512/sha512";
|
||||
|
||||
async function withFallback(data: Uint8Array): Promise<Uint8Array> {
|
||||
try {
|
||||
return sha512(data);
|
||||
} catch {
|
||||
// Fall back to WebCrypto if native primitive is unavailable.
|
||||
const buffer = await crypto.subtle.digest("SHA-512", data);
|
||||
return new Uint8Array(buffer);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `sha512(_data: Uint8Array): Uint8Array`
|
||||
|
||||
Placeholder that always throws.
|
||||
|
||||
| Parameter | Type | Description |
|
||||
|-----------|------|-------------|
|
||||
| `_data` | `Uint8Array` | Ignored. |
|
||||
|
||||
**Throws:** `PrimitiveNotImplemented("sha512")` — always.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The underscore prefix on `_data` marks the parameter as intentionally unused without a lint suppression comment. When the in-house SHA-512 implementation is complete, this file is updated with the real algorithm, the underscore removed, and the selector regenerated. The `PrimitiveNotImplemented` error class in `@suaveplan/crypto-utils/errors` extends `SuaveplanError`, so it participates in the full error serialisation and cause-chain system.
|
||||
|
||||
### Status
|
||||
|
||||
Placeholder native stub — throws `PrimitiveNotImplemented` until a `node:crypto` / WebCrypto `SHA-512` bridge is wired up. Today the `sha512.select` selector always routes through Noble; the SHA-512 KAT harness in `@suaveplan/crypto-utils` pins the output against NIST FIPS 180-4 test vectors so the production path is fully covered.
|
||||
@@ -0,0 +1,62 @@
|
||||
# sha512.noble
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
`sha512.noble.ts` is the Noble-backed concrete implementation of the SHA-512 primitive in `@suaveplan/crypto-hash`. It wraps `sha512` from `@noble/hashes/sha2`, producing 64-byte digests with the same one-shot interface expected by the selector and public façade.
|
||||
|
||||
SHA-512 is the widest SHA-2 variant and is used as the underlying hash in HMAC-SHA-512, HKDF-SHA-512, and PBKDF2-SHA-512. Its 64-bit word operations run natively fast on 64-bit processors and provide the largest security margin in the SHA-2 family. This file is the production-ready side of the two-implementation layout.
|
||||
|
||||
## Features
|
||||
|
||||
- Delegates entirely to `@noble/hashes/sha2` SHA-512 implementation.
|
||||
- Returns a fresh 64-byte `Uint8Array` on every call.
|
||||
- Zero side effects; no global state or lazy initialisation.
|
||||
- Tree-shakeable: unreachable when the native backend is selected.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { sha512 } from "@suaveplan/crypto-hash/primitives/sha512/sha512";
|
||||
|
||||
const digest = sha512(new TextEncoder().encode("hello"));
|
||||
console.log(digest.length); // 64
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { sha512 } from "@suaveplan/crypto-hash/primitives/sha512/sha512";
|
||||
import { toHex } from "@suaveplan/crypto-hash/encoding";
|
||||
|
||||
// Produce a 512-bit content fingerprint for audit logging.
|
||||
function auditDigest(payload: Uint8Array, label: string): string {
|
||||
const combined = new Uint8Array(payload.length + label.length);
|
||||
combined.set(new TextEncoder().encode(label), 0);
|
||||
combined.set(payload, label.length);
|
||||
return toHex(sha512(combined));
|
||||
}
|
||||
|
||||
const hex = auditDigest(new Uint8Array([0x01]), "event:");
|
||||
console.log(hex.length); // 128 hex chars
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `sha512(data: Uint8Array): Uint8Array`
|
||||
|
||||
Computes the SHA-512 digest of `data`.
|
||||
|
||||
| Parameter | Type | Description |
|
||||
|-----------|------|-------------|
|
||||
| `data` | `Uint8Array` | Arbitrary byte sequence. |
|
||||
|
||||
**Returns:** Fresh 64-byte `Uint8Array`.
|
||||
|
||||
**Throws:** Never.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The function is `return nobleSha512(data)` — a direct pass-through. Noble handles buffer allocation internally; the returned array is never an alias of the input. SHA-512 applies 80 rounds of compression over a 1024-bit block, producing a 512-bit state, all in 64-bit arithmetic. The Noble implementation is audited and passes NIST FIPS 180-4 CAVP vectors. No streaming interface is provided at this layer.
|
||||
@@ -0,0 +1,60 @@
|
||||
# sha512.select
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
`sha512.select.ts` is the build-time primitive selector for the SHA-512 implementation. It is a generated file that re-exports exactly one concrete implementation — either `sha512.noble.ts` (the audited `@noble/hashes` backend) or `sha512.native.ts` — based on the active entry in `crypto.config.ts`.
|
||||
|
||||
SHA-512 produces 64-byte digests and is the largest of the SHA-2 family. It is used internally by HMAC-SHA-512, HKDF-SHA-512, and PBKDF2-SHA-512 within this package, and is exposed as a standalone primitive for callers that need a wider security margin than SHA-256 provides.
|
||||
|
||||
## Features
|
||||
|
||||
- Single-symbol re-export of `sha512(data: Uint8Array): Uint8Array`.
|
||||
- Generated by `bun scripts/gen-crypto-selects.ts` — never hand-edited.
|
||||
- Controlled by `crypto.config.ts`: set `sha512` to `'noble'` or `'native'`.
|
||||
- Maintains a stable public API across backend switches.
|
||||
- Enables static dead-code elimination of the unused backend.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { sha512 } from "@suaveplan/crypto-hash/primitives/sha512/sha512";
|
||||
|
||||
const digest = sha512(new TextEncoder().encode("hello"));
|
||||
console.log(digest.length); // 64
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { sha512 } from "@suaveplan/crypto-hash/primitives/sha512/sha512";
|
||||
import { toHex } from "@suaveplan/crypto-hash/encoding";
|
||||
|
||||
// Compute a hex-encoded SHA-512 fingerprint of a binary blob.
|
||||
function fingerprint(blob: Uint8Array): string {
|
||||
return `sha512:${toHex(sha512(blob))}`;
|
||||
}
|
||||
|
||||
const fp = fingerprint(new Uint8Array([0xde, 0xad, 0xbe, 0xef]));
|
||||
console.log(fp.startsWith("sha512:")); // true
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
Re-exports the following from the active implementation:
|
||||
|
||||
### `sha512(data: Uint8Array): Uint8Array`
|
||||
|
||||
One-shot SHA-512 digest.
|
||||
|
||||
| Parameter | Type | Description |
|
||||
|-----------|------|-------------|
|
||||
| `data` | `Uint8Array` | Arbitrary byte sequence to hash. |
|
||||
|
||||
**Returns:** Fresh 64-byte `Uint8Array` containing the SHA-512 digest.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
SHA-512 uses 80 rounds of 64-bit operations on a 1024-bit block, giving it an internal state twice the width of SHA-256. On 64-bit hardware this is as fast or faster than SHA-256 for large inputs. The Noble backend passes NIST CAVP and RFC 6234 test vectors without modification. The native stub throws `PrimitiveNotImplemented("sha512")` and will be activated once the in-house implementation lands with full KAT coverage. The selector file itself is a generated artefact; the generator is idempotent and safe to re-run.
|
||||
Reference in New Issue
Block a user