Audited & minimal 0-dependency JS implementation of SHA, RIPEMD, BLAKE, HMAC, HKDF, PBKDF & Scrypt
HKDF is a key derivation function that extracts and expands cryptographic key material. It transforms input keying material (IKM) into one or more cryptographically strong secret keys suitable for use in cryptographic operations.
import { hkdf, extract, expand } from '@noble/hashes/hkdf.js';
import { sha256, sha512 } from '@noble/hashes/sha2.js';
import { sha3_256 } from '@noble/hashes/sha3.js';The main hkdf function performs both extract and expand steps in one call.
/**
* HKDF key derivation function (extract + expand)
* @param hash - Hash function to use (e.g., sha256, sha512)
* @param ikm - Input keying material as Uint8Array
* @param salt - Optional salt value (recommended, defaults to hash.outputLen zeros)
* @param info - Optional context and application specific information
* @param length - Desired output length in bytes (must be <= 255 * hash.outputLen)
* @returns Derived key material
*/
function hkdf(
hash: CHash,
ikm: Uint8Array,
salt: Uint8Array | undefined,
info: Uint8Array | undefined,
length: number
): Uint8Array;The extract step concentrates entropy from the input keying material into a fixed-length pseudorandom key (PRK).
/**
* HKDF extract step: concentrates entropy from IKM
* @param hash - Hash function to use
* @param ikm - Input keying material as Uint8Array
* @param salt - Optional salt value (defaults to hash.outputLen zeros)
* @returns Pseudorandom key (PRK) of hash.outputLen bytes
*/
function extract(
hash: CHash,
ikm: Uint8Array,
salt?: Uint8Array
): Uint8Array;The expand step derives multiple keys from the pseudorandom key with optional context information.
/**
* HKDF expand step: expands PRK into output keying material
* @param hash - Hash function to use
* @param prk - Pseudorandom key from extract step
* @param info - Optional context and application specific information
* @param length - Desired output length in bytes (defaults to hash.outputLen)
* @returns Output keying material (OKM)
*/
function expand(
hash: CHash,
prk: Uint8Array,
info?: Uint8Array,
length?: number
): Uint8Array;import { hkdf } from '@noble/hashes/hkdf.js';
import { sha256 } from '@noble/hashes/sha2.js';
import { randomBytes } from '@noble/hashes/utils.js';
// Generate input keying material (e.g., from ECDH, password hash, etc.)
const ikm = randomBytes(32);
// Optional salt (recommended)
const salt = randomBytes(32);
// Optional context information
const info = new TextEncoder().encode('application-key-v1');
// Derive 32-byte key
const derivedKey = hkdf(sha256, ikm, salt, info, 32);import { extract, expand } from '@noble/hashes/hkdf.js';
import { sha256 } from '@noble/hashes/sha2.js';
import { utf8ToBytes } from '@noble/hashes/utils.js';
// Step 1: Extract PRK once
const ikm = new Uint8Array(32).fill(0x42);
const salt = new Uint8Array(32).fill(0x01);
const prk = extract(sha256, ikm, salt);
// Step 2: Expand into multiple keys with different contexts
const encryptionKey = expand(sha256, prk, utf8ToBytes('encryption'), 32);
const authKey = expand(sha256, prk, utf8ToBytes('authentication'), 32);
const signingKey = expand(sha256, prk, utf8ToBytes('signing'), 32);
// Each key is cryptographically independentimport { hkdf } from '@noble/hashes/hkdf.js';
import { sha256 } from '@noble/hashes/sha2.js';
import { utf8ToBytes, concatBytes } from '@noble/hashes/utils.js';
interface SessionKeys {
clientWriteKey: Uint8Array;
serverWriteKey: Uint8Array;
clientWriteIV: Uint8Array;
serverWriteIV: Uint8Array;
}
function deriveSessionKeys(
sharedSecret: Uint8Array,
clientRandom: Uint8Array,
serverRandom: Uint8Array
): SessionKeys {
// Use both randoms as salt
const salt = concatBytes(clientRandom, serverRandom);
// Context information
const info = utf8ToBytes('session-keys-v1');
// Derive key material (128 bytes total)
const keyMaterial = hkdf(sha256, sharedSecret, salt, info, 128);
// Split into different keys
return {
clientWriteKey: keyMaterial.slice(0, 32),
serverWriteKey: keyMaterial.slice(32, 64),
clientWriteIV: keyMaterial.slice(64, 80),
serverWriteIV: keyMaterial.slice(80, 96)
};
}import { hkdf } from '@noble/hashes/hkdf.js';
import { sha256 } from '@noble/hashes/sha2.js';
import { pbkdf2 } from '@noble/hashes/pbkdf2.js';
import { utf8ToBytes } from '@noble/hashes/utils.js';
async function deriveKeysFromPassword(
password: string,
salt: Uint8Array
): Promise<{
encryptionKey: Uint8Array;
authKey: Uint8Array;
}> {
// First, strengthen password with PBKDF2
const ikm = pbkdf2(sha256, password, salt, { c: 100000, dkLen: 32 });
// Then derive multiple keys with HKDF
const encryptionKey = hkdf(
sha256,
ikm,
salt,
utf8ToBytes('encryption'),
32
);
const authKey = hkdf(
sha256,
ikm,
salt,
utf8ToBytes('authentication'),
32
);
return { encryptionKey, authKey };
}import { hkdf } from '@noble/hashes/hkdf.js';
import { sha256 } from '@noble/hashes/sha2.js';
import { utf8ToBytes } from '@noble/hashes/utils.js';
function rekeySession(
currentKey: Uint8Array,
messageCount: number
): Uint8Array {
// Use message count as context for unique keys
const info = concatBytes(
utf8ToBytes('rekey-'),
new Uint8Array([messageCount >> 24, messageCount >> 16, messageCount >> 8, messageCount])
);
// Derive new key from current key
return hkdf(sha256, currentKey, undefined, info, 32);
}
// Usage: rekey after every 1000 messages
let sessionKey = initialKey;
let messageCount = 0;
function sendMessage(data: Uint8Array) {
// Use current key for encryption
encrypt(sessionKey, data);
messageCount++;
if (messageCount % 1000 === 0) {
sessionKey = rekeySession(sessionKey, messageCount);
}
}import { hkdf } from '@noble/hashes/hkdf.js';
import { sha256 } from '@noble/hashes/sha2.js';
import { utf8ToBytes } from '@noble/hashes/utils.js';
interface CommittedKey {
key: Uint8Array;
commitment: Uint8Array;
}
function deriveCommittedKey(ikm: Uint8Array, salt: Uint8Array): CommittedKey {
// Derive both key and commitment
const material = hkdf(sha256, ikm, salt, utf8ToBytes('committed'), 64);
return {
key: material.slice(0, 32),
commitment: material.slice(32, 64) // Public commitment to key
};
}
// Recipient can verify commitment without knowing the key
function verifyCommitment(
commitment: Uint8Array,
revealedKey: Uint8Array,
ikm: Uint8Array,
salt: Uint8Array
): boolean {
const { key, commitment: expectedCommitment } = deriveCommittedKey(ikm, salt);
// Check if revealed key and commitment match
if (key.length !== revealedKey.length) return false;
if (commitment.length !== expectedCommitment.length) return false;
let diff = 0;
for (let i = 0; i < key.length; i++) {
diff |= key[i] ^ revealedKey[i];
}
for (let i = 0; i < commitment.length; i++) {
diff |= commitment[i] ^ expectedCommitment[i];
}
return diff === 0;
}import { hkdf } from '@noble/hashes/hkdf.js';
import { sha256 } from '@noble/hashes/sha2.js';
import { utf8ToBytes } from '@noble/hashes/utils.js';
class HierarchicalKeyDerivation {
private masterKey: Uint8Array;
constructor(masterKey: Uint8Array) {
this.masterKey = masterKey;
}
deriveLevel1(context: string): Uint8Array {
return hkdf(
sha256,
this.masterKey,
undefined,
utf8ToBytes(`level1:${context}`),
32
);
}
deriveLevel2(level1Key: Uint8Array, context: string): Uint8Array {
return hkdf(
sha256,
level1Key,
undefined,
utf8ToBytes(`level2:${context}`),
32
);
}
deriveLevel3(level2Key: Uint8Array, index: number): Uint8Array {
const info = new Uint8Array(12);
new TextEncoder().encode('level3:').forEach((b, i) => info[i] = b);
new DataView(info.buffer).setUint32(7, index, false);
return hkdf(sha256, level2Key, undefined, info, 32);
}
}
// Usage
const masterKey = new Uint8Array(32).fill(0x42);
const kdf = new HierarchicalKeyDerivation(masterKey);
const userKey = kdf.deriveLevel1('user-123');
const deviceKey = kdf.deriveLevel2(userKey, 'device-456');
const sessionKey = kdf.deriveLevel3(deviceKey, 1);HKDF follows a two-step process:
Extract: PRK = HMAC-Hash(salt, IKM)
Expand: OKM = HMAC-Hash(PRK, info || 0x01) || HMAC-Hash(PRK, prev || info || 0x02) || ...
IKM (Input Keying Material):
Salt:
Info:
Length:
PRK Security: The extract step ensures PRK has at least min(hash.outputLen, IKM_entropy) bits of security.
Output Independence: Different info values produce cryptographically independent outputs.
Forward Security: Compromising OKM doesn't reveal IKM or PRK (if extract used salt).
Length Extension Immunity: HMAC construction prevents length extension attacks.
Use full HKDF (extract + expand):
Use extract separately:
Use expand separately:
HKDF overhead depends on output length:
Benchmarks on Apple M4:
hkdf(sha256, ikm, salt, info, 32): ~3μsextract(sha256, ikm, salt): ~1μsexpand(sha256, prk, info, 32): ~1μsFor multiple derived keys, extract once and expand multiple times:
// Efficient: One extract, multiple expands
const prk = extract(sha256, ikm, salt);
const key1 = expand(sha256, prk, info1, 32); // ~1μs
const key2 = expand(sha256, prk, info2, 32); // ~1μs
const key3 = expand(sha256, prk, info3, 32); // ~1μs
// Inefficient: Three full HKDFs
const key1 = hkdf(sha256, ikm, salt, info1, 32); // ~3μs
const key2 = hkdf(sha256, ikm, salt, info2, 32); // ~3μs
const key3 = hkdf(sha256, ikm, salt, info3, 32); // ~3μsHKDF advantages:
vs PBKDF2:
vs KDF in Expand Mode:
Pattern 1: DH/ECDH Key Agreement
const sharedSecret = performDHExchange();
const derivedKey = hkdf(sha256, sharedSecret, publicSalt, protocolInfo, 32);Pattern 2: Master Key → Session Keys
const prk = extract(sha256, masterKey, sessionSalt);
const encKey = expand(sha256, prk, utf8ToBytes('enc'), 32);
const macKey = expand(sha256, prk, utf8ToBytes('mac'), 32);Pattern 3: Password → Multiple Keys
const passwordHash = pbkdf2(sha256, password, salt, { c: 100000, dkLen: 32 });
const keys = hkdf(sha256, passwordHash, salt, protocolInfo, 64);Avoid legacy hashes (MD5, SHA-1) even though HKDF remains relatively secure with them.