CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/npm-noble--hashes

Audited & minimal 0-dependency JS implementation of SHA, RIPEMD, BLAKE, HMAC, HKDF, PBKDF & Scrypt

Overview
Eval results
Files

hkdf.mddocs/

HKDF: HMAC-based Key Derivation Function

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.

Imports

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';

Capabilities

Combined HKDF Function

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;

Extract Step

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;

Expand Step

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;

Usage Examples

Basic Key Derivation

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);

Deriving Multiple Keys

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 independent

Protocol Key Establishment

import { 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)
  };
}

Key Derivation from Password Hash

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 };
}

Rekeying

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);
  }
}

Key Commitment

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;
}

Hierarchical Key Derivation

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);

Technical Details

HKDF Design

HKDF follows a two-step process:

  1. Extract: PRK = HMAC-Hash(salt, IKM)

    • Concentrates entropy into a pseudorandom key
    • Salt should be random but can be public
    • Output is always hash.outputLen bytes
  2. Expand: OKM = HMAC-Hash(PRK, info || 0x01) || HMAC-Hash(PRK, prev || info || 0x02) || ...

    • Expands PRK into arbitrary-length output
    • Info provides context separation
    • Counter ensures unique outputs for each block

Input Parameters

IKM (Input Keying Material):

  • Source of cryptographic entropy
  • Can be from: ECDH, DH, password hash, master key, etc.
  • Should have at least hash.outputLen bits of entropy
  • Can be any length

Salt:

  • Optional but strongly recommended
  • Should be random and independent of IKM
  • Can be public (not secret)
  • Defaults to hash.outputLen zero bytes if not provided
  • Improves security when IKM has low entropy

Info:

  • Optional context and application-specific information
  • Can include: protocol version, purpose, session ID, etc.
  • Provides key separation (different contexts → different keys)
  • Can be public (not secret)
  • Can be empty

Length:

  • Desired output length in bytes
  • Maximum: 255 × hash.outputLen bytes
    • SHA-256: max 8160 bytes (255 × 32)
    • SHA-512: max 16320 bytes (255 × 64)

Security Properties

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.

When to Use Extract vs Expand

Use full HKDF (extract + expand):

  • IKM comes from uncertain sources (DH, ECDH)
  • You need single key from single source
  • Simple use case

Use extract separately:

  • IKM source varies (sometimes high-entropy, sometimes low)
  • Want to normalize all IKMs to fixed-length PRK

Use expand separately:

  • PRK already available from another KDF
  • Deriving multiple keys from same source
  • Reduces computational overhead (one extract, multiple expands)

Performance

HKDF overhead depends on output length:

  • Extract: One HMAC operation
  • Expand: Ceiling(length / hash.outputLen) HMAC operations

Benchmarks on Apple M4:

  • hkdf(sha256, ikm, salt, info, 32): ~3μs
  • extract(sha256, ikm, salt): ~1μs
  • expand(sha256, prk, info, 32): ~1μs

For 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μs

HKDF vs Other KDFs

HKDF advantages:

  • Simple construction
  • Efficient for multiple keys
  • Flexible output length
  • Works with any hash function

vs PBKDF2:

  • HKDF is NOT for passwords (no iteration count)
  • Use PBKDF2/Scrypt/Argon2 for passwords, then HKDF for key derivation

vs KDF in Expand Mode:

  • HKDF has formal security proof
  • Some protocols use expand-only mode (e.g., TLS 1.3)

Common Patterns

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);

Recommended Hash Functions

  • SHA-256: Standard choice, widely supported
  • SHA-512: Better security margin, faster on 64-bit
  • SHA3-256: Alternative design, NIST standard
  • BLAKE2b: Fastest, good security

Avoid legacy hashes (MD5, SHA-1) even though HKDF remains relatively secure with them.

References

  • RFC 5869: HMAC-based Extract-and-Expand Key Derivation Function
  • HKDF Paper: Cryptographic Extraction and Key Derivation

Install with Tessl CLI

npx tessl i tessl/npm-noble--hashes

docs

argon2.md

blake.md

eskdf.md

hkdf.md

hmac.md

index.md

legacy.md

pbkdf2.md

scrypt.md

sha2.md

sha3-addons.md

sha3.md

utils.md

webcrypto.md

tile.json