Initial Commit
This commit is contained in:
106
packages/security-stack/crypto/crypto-hash/src/kdf/hkdf/hkdf.md
Normal file
106
packages/security-stack/crypto/crypto-hash/src/kdf/hkdf/hkdf.md
Normal file
@@ -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,80 @@
|
||||
import { describe, expect, it } from "@suaveplan/testing/runner";
|
||||
import fc from "fast-check";
|
||||
import { hkdf } from "./hkdf.js";
|
||||
|
||||
describe("hkdf", () => {
|
||||
it("produces the requested number of bytes for SHA-256", {
|
||||
timeout: 5000,
|
||||
}, () => {
|
||||
const out = hkdf("SHA-256", new Uint8Array(32).fill(1), {
|
||||
salt: new Uint8Array(16).fill(2),
|
||||
info: new Uint8Array([3]),
|
||||
length: 42,
|
||||
});
|
||||
expect(out.length).toBe(42);
|
||||
});
|
||||
|
||||
it("is deterministic for SHA-256", { timeout: 5000 }, () => {
|
||||
const opts = {
|
||||
salt: new Uint8Array(16),
|
||||
info: new Uint8Array(4),
|
||||
length: 32,
|
||||
};
|
||||
const a = hkdf("SHA-256", new Uint8Array(32), opts);
|
||||
const b = hkdf("SHA-256", new Uint8Array(32), opts);
|
||||
expect(a).toEqual(b);
|
||||
});
|
||||
|
||||
it("is deterministic for SHA-512", { timeout: 5000 }, () => {
|
||||
const opts = {
|
||||
salt: new Uint8Array(16),
|
||||
info: new Uint8Array(4),
|
||||
length: 32,
|
||||
};
|
||||
const a = hkdf("SHA-512", new Uint8Array(32), opts);
|
||||
const b = hkdf("SHA-512", new Uint8Array(32), opts);
|
||||
expect(a).toEqual(b);
|
||||
});
|
||||
|
||||
it("covers SHA-384 path", { timeout: 5000 }, () => {
|
||||
const out = hkdf("SHA-384", new Uint8Array(32), {
|
||||
salt: new Uint8Array(16),
|
||||
info: new Uint8Array(0),
|
||||
length: 16,
|
||||
});
|
||||
expect(out.length).toBe(16);
|
||||
});
|
||||
|
||||
it("same inputs produce same outputs across algorithms", {
|
||||
timeout: 5000,
|
||||
}, () => {
|
||||
const ikm = new Uint8Array(32).fill(7);
|
||||
const salt = new Uint8Array(16).fill(3);
|
||||
const info = new Uint8Array([1, 2]);
|
||||
// Different algorithms produce different outputs
|
||||
const out256 = hkdf("SHA-256", ikm, { salt, info, length: 32 });
|
||||
const out512 = hkdf("SHA-512", ikm, { salt, info, length: 32 });
|
||||
expect(out256).not.toEqual(out512);
|
||||
});
|
||||
|
||||
it("property: HKDF is deterministic", { timeout: 10000 }, () => {
|
||||
fc.assert(
|
||||
fc.property(
|
||||
fc.uint8Array({ minLength: 1 }),
|
||||
fc.uint8Array(),
|
||||
fc.uint8Array(),
|
||||
fc.integer({ min: 1, max: 64 }),
|
||||
(ikm, salt, info, length) => {
|
||||
const opts = {
|
||||
salt: Uint8Array.from(salt),
|
||||
info: Uint8Array.from(info),
|
||||
length,
|
||||
};
|
||||
const a = hkdf("SHA-256", Uint8Array.from(ikm), opts);
|
||||
const b = hkdf("SHA-256", Uint8Array.from(ikm), opts);
|
||||
return a.every((v, i) => v === b[i]);
|
||||
}
|
||||
)
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,21 @@
|
||||
/**
|
||||
* Public façade for HKDF. Routes through the primitive selector layer.
|
||||
*/
|
||||
|
||||
import type { HashAlgorithm } from "../../hash/sha2/sha2.js";
|
||||
import { hkdf as selectedHkdf } from "../../primitives/hkdf/hkdf.select/hkdf.select.js";
|
||||
|
||||
export interface HkdfOptions {
|
||||
readonly salt: Uint8Array;
|
||||
readonly info: Uint8Array;
|
||||
readonly length: number;
|
||||
}
|
||||
|
||||
/** HKDF-Extract-and-Expand over SHA-256/384/512. */
|
||||
export function hkdf(
|
||||
algo: HashAlgorithm,
|
||||
ikm: Uint8Array,
|
||||
options: HkdfOptions
|
||||
): Uint8Array {
|
||||
return selectedHkdf(algo, ikm, options);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { type HkdfOptions, hkdf } from "./hkdf.js";
|
||||
Reference in New Issue
Block a user