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

pbkdf2.mddocs/

PBKDF2: Password-Based Key Derivation Function 2

PBKDF2 derives cryptographic keys from passwords through repeated application of a pseudorandom function (HMAC). The iteration count makes brute-force attacks computationally expensive, providing protection for user passwords.

Imports

import { pbkdf2, pbkdf2Async } from '@noble/hashes/pbkdf2.js';
import { sha256, sha512 } from '@noble/hashes/sha2.js';
import { sha3_256 } from '@noble/hashes/sha3.js';

Capabilities

Synchronous PBKDF2

Blocks execution until complete. Suitable for server environments or when blocking is acceptable.

/**
 * PBKDF2 key derivation (synchronous)
 * @param hash - Hash function to use (e.g., sha256, sha512)
 * @param password - Password as string or Uint8Array
 * @param salt - Salt as string or Uint8Array (should be random, at least 16 bytes)
 * @param opts - Options
 * @param opts.c - Iteration count (>= 1, recommended: 100000+)
 * @param opts.dkLen - Desired key length in bytes (default: 32)
 * @returns Derived key
 */
function pbkdf2(
  hash: CHash,
  password: string | Uint8Array,
  salt: string | Uint8Array,
  opts: Pbkdf2Opt
): Uint8Array;

/**
 * Options for PBKDF2
 */
interface Pbkdf2Opt {
  /** Iteration count (>= 1) - higher is more secure but slower */
  c: number;
  /** Desired output length in bytes (default: 32) */
  dkLen?: number;
}

Asynchronous PBKDF2

Non-blocking, yields to event loop periodically. Recommended for browsers and UI applications.

/**
 * PBKDF2 key derivation (asynchronous)
 * @param hash - Hash function to use (e.g., sha256, sha512)
 * @param password - Password as string or Uint8Array
 * @param salt - Salt as string or Uint8Array (should be random, at least 16 bytes)
 * @param opts - Options
 * @param opts.c - Iteration count (>= 1, recommended: 100000+)
 * @param opts.dkLen - Desired key length in bytes (default: 32)
 * @param opts.asyncTick - Max milliseconds before yielding to event loop (default: 10)
 * @returns Promise resolving to derived key
 */
function pbkdf2Async(
  hash: CHash,
  password: string | Uint8Array,
  salt: string | Uint8Array,
  opts: Pbkdf2Opt
): Promise<Uint8Array>;

/**
 * Options for asynchronous PBKDF2
 */
interface Pbkdf2Opt {
  /** Iteration count (>= 1) - higher is more secure but slower */
  c: number;
  /** Desired output length in bytes (default: 32) */
  dkLen?: number;
  /** Max blocking time in ms before yielding (default: 10) */
  asyncTick?: number;
}

Usage Examples

Basic Password Hashing

import { pbkdf2 } from '@noble/hashes/pbkdf2.js';
import { sha256 } from '@noble/hashes/sha2.js';
import { randomBytes, bytesToHex, hexToBytes } from '@noble/hashes/utils.js';

// Generate random salt
const salt = randomBytes(16);

// Derive key from password
const key = pbkdf2(sha256, 'user-password', salt, {
  c: 100000,  // 100k iterations (recommended minimum)
  dkLen: 32   // 32-byte key
});

// Store salt and key for later verification
const saltHex = bytesToHex(salt);
const keyHex = bytesToHex(key);

// Verification
function verifyPassword(
  password: string,
  storedSalt: string,
  storedKey: string
): boolean {
  const salt = hexToBytes(storedSalt);
  const expectedKey = hexToBytes(storedKey);

  const derivedKey = pbkdf2(sha256, password, salt, {
    c: 100000,
    dkLen: 32
  });

  // Constant-time comparison
  if (derivedKey.length !== expectedKey.length) return false;

  let diff = 0;
  for (let i = 0; i < derivedKey.length; i++) {
    diff |= derivedKey[i] ^ expectedKey[i];
  }
  return diff === 0;
}

Asynchronous Password Hashing (Browser-Friendly)

import { pbkdf2Async } from '@noble/hashes/pbkdf2.js';
import { sha256 } from '@noble/hashes/sha2.js';
import { randomBytes } from '@noble/hashes/utils.js';

async function hashPasswordAsync(password: string): Promise<{
  salt: Uint8Array;
  key: Uint8Array;
}> {
  const salt = randomBytes(16);

  // Non-blocking - UI remains responsive
  const key = await pbkdf2Async(sha256, password, salt, {
    c: 100000,
    dkLen: 32,
    asyncTick: 10  // Yield every 10ms
  });

  return { salt, key };
}

async function verifyPasswordAsync(
  password: string,
  salt: Uint8Array,
  expectedKey: Uint8Array
): Promise<boolean> {
  const derivedKey = await pbkdf2Async(sha256, password, salt, {
    c: 100000,
    dkLen: 32
  });

  // Constant-time comparison
  if (derivedKey.length !== expectedKey.length) return false;

  let diff = 0;
  for (let i = 0; i < derivedKey.length; i++) {
    diff |= derivedKey[i] ^ expectedKey[i];
  }
  return diff === 0;
}

Encryption Key from Password

import { pbkdf2 } from '@noble/hashes/pbkdf2.js';
import { sha256 } from '@noble/hashes/sha2.js';
import { randomBytes } from '@noble/hashes/utils.js';

interface EncryptionKey {
  key: Uint8Array;
  salt: Uint8Array;
}

function deriveEncryptionKey(password: string): EncryptionKey {
  const salt = randomBytes(16);

  const key = pbkdf2(sha256, password, salt, {
    c: 100000,
    dkLen: 32
  });

  return { key, salt };
}

function restoreEncryptionKey(password: string, salt: Uint8Array): Uint8Array {
  return pbkdf2(sha256, password, salt, {
    c: 100000,
    dkLen: 32
  });
}

// Usage
const { key: encKey, salt } = deriveEncryptionKey('my-secret-password');

// Later, restore key from password and salt
const restoredKey = restoreEncryptionKey('my-secret-password', salt);

Multiple Keys from One Password

import { pbkdf2 } from '@noble/hashes/pbkdf2.js';
import { sha256 } from '@noble/hashes/sha2.js';
import { hkdf } from '@noble/hashes/hkdf.js';
import { utf8ToBytes } from '@noble/hashes/utils.js';

function deriveMultipleKeys(
  password: string,
  salt: Uint8Array
): {
  encryptionKey: Uint8Array;
  authKey: Uint8Array;
  signingKey: Uint8Array;
} {
  // Step 1: PBKDF2 to strengthen password
  const masterKey = pbkdf2(sha256, password, salt, {
    c: 100000,
    dkLen: 32
  });

  // Step 2: HKDF to derive multiple keys
  const encryptionKey = hkdf(
    sha256,
    masterKey,
    salt,
    utf8ToBytes('encryption'),
    32
  );

  const authKey = hkdf(
    sha256,
    masterKey,
    salt,
    utf8ToBytes('authentication'),
    32
  );

  const signingKey = hkdf(
    sha256,
    masterKey,
    salt,
    utf8ToBytes('signing'),
    32
  );

  return { encryptionKey, authKey, signingKey };
}

Password Storage Format

import { pbkdf2Async } from '@noble/hashes/pbkdf2.js';
import { sha256 } from '@noble/hashes/sha2.js';
import { randomBytes, bytesToHex, hexToBytes } from '@noble/hashes/utils.js';

interface PasswordHash {
  algorithm: string;
  iterations: number;
  salt: string;
  hash: string;
}

async function hashPassword(password: string): Promise<PasswordHash> {
  const salt = randomBytes(16);
  const iterations = 100000;

  const hash = await pbkdf2Async(sha256, password, salt, {
    c: iterations,
    dkLen: 32
  });

  return {
    algorithm: 'pbkdf2-sha256',
    iterations: iterations,
    salt: bytesToHex(salt),
    hash: bytesToHex(hash)
  };
}

async function verifyPassword(
  password: string,
  stored: PasswordHash
): Promise<boolean> {
  if (stored.algorithm !== 'pbkdf2-sha256') {
    throw new Error('Unsupported algorithm');
  }

  const salt = hexToBytes(stored.salt);
  const expectedHash = hexToBytes(stored.hash);

  const derivedHash = await pbkdf2Async(sha256, password, salt, {
    c: stored.iterations,
    dkLen: expectedHash.length
  });

  // Constant-time comparison
  if (derivedHash.length !== expectedHash.length) return false;

  let diff = 0;
  for (let i = 0; i < derivedHash.length; i++) {
    diff |= derivedHash[i] ^ expectedHash[i];
  }
  return diff === 0;
}

// Serialization for database storage
function serializePasswordHash(hash: PasswordHash): string {
  return `${hash.algorithm}$${hash.iterations}$${hash.salt}$${hash.hash}`;
}

function parsePasswordHash(serialized: string): PasswordHash {
  const [algorithm, iterations, salt, hash] = serialized.split('$');
  return {
    algorithm,
    iterations: parseInt(iterations),
    salt,
    hash
  };
}

Adaptive Iteration Count

import { pbkdf2 } from '@noble/hashes/pbkdf2.js';
import { sha256 } from '@noble/hashes/sha2.js';
import { randomBytes } from '@noble/hashes/utils.js';

function calibrateIterations(targetMs: number = 100): number {
  const testSalt = randomBytes(16);
  const testPassword = 'test-password';

  // Binary search for iteration count
  let low = 10000;
  let high = 1000000;
  let best = 100000;

  while (high - low > 10000) {
    const mid = Math.floor((low + high) / 2);
    const start = performance.now();

    pbkdf2(sha256, testPassword, testSalt, { c: mid, dkLen: 32 });

    const elapsed = performance.now() - start;

    if (elapsed < targetMs) {
      low = mid;
    } else {
      high = mid;
      best = mid;
    }
  }

  return best;
}

// Usage: determine optimal iteration count for this device
const optimalIterations = calibrateIterations(100); // Target 100ms
console.log('Optimal iterations:', optimalIterations);

Technical Details

PBKDF2 Algorithm

PBKDF2 applies a pseudorandom function (HMAC) iteratively:

DK = PBKDF2(PRF, Password, Salt, c, dkLen)

T_i = F(Password, Salt, c, i) where i = 1, 2, ..., ceiling(dkLen / hLen)

F(Password, Salt, c, i) = U_1 ⊕ U_2 ⊕ ... ⊕ U_c

U_1 = PRF(Password, Salt || INT_32_BE(i))
U_2 = PRF(Password, U_1)
U_c = PRF(Password, U_{c-1})

DK = T_1 || T_2 || ... || T_ceiling(dkLen/hLen)<0..dkLen-1>

Parameter Selection

Iteration Count (c):

  • Minimum: 100,000 (OWASP 2023 recommendation for PBKDF2-SHA256)
  • Recommended: 600,000+ for password storage (OWASP 2023)
  • Enterprise: 1,000,000+
  • Trade-off: Higher is more secure but slower

Time on Apple M4 (2024):

  • 100,000 iterations: ~40ms
  • 600,000 iterations: ~240ms
  • 1,000,000 iterations: ~400ms

Salt Length:

  • Minimum: 16 bytes (128 bits)
  • Recommended: 32 bytes (256 bits)
  • Must be random and unique per password
  • Can be stored in plaintext alongside hash

Output Length (dkLen):

  • Minimum: 16 bytes (128 bits)
  • Recommended: 32 bytes (256 bits)
  • Should match your security requirements

Hash Function Selection

SHA-256 (recommended):

  • Widely supported
  • Good balance of security and performance
  • FIPS compliant

SHA-512:

  • Better security margin
  • Faster on 64-bit platforms
  • Longer output (64 bytes default)

SHA-3 variants:

  • Alternative design
  • Good for defense in depth
  • Slightly slower
import { pbkdf2 } from '@noble/hashes/pbkdf2.js';
import { sha256, sha512 } from '@noble/hashes/sha2.js';
import { sha3_256 } from '@noble/hashes/sha3.js';

const password = 'my-password';
const salt = randomBytes(16);

// Different hash functions
const key256 = pbkdf2(sha256, password, salt, { c: 100000, dkLen: 32 });
const key512 = pbkdf2(sha512, password, salt, { c: 100000, dkLen: 32 });
const keySha3 = pbkdf2(sha3_256, password, salt, { c: 100000, dkLen: 32 });

Security Considerations

Salt Requirements:

  • Must be cryptographically random
  • Must be unique per password
  • Should be at least 16 bytes
  • Can be stored in plaintext
  • Prevents rainbow table attacks
  • Prevents parallel attacks on multiple passwords

Iteration Count:

  • Higher count increases attack cost linearly
  • Should be as high as user experience allows
  • Should be increased over time as hardware improves
  • Store iteration count with hash for upgradability

Password Input:

  • Accept strings or Uint8Array
  • Strings are UTF-8 encoded internally
  • No password length limit
  • Longer passwords are not slower

Constant-Time Comparison: Always compare hashes in constant time to prevent timing attacks:

function constantTimeEqual(a: Uint8Array, b: Uint8Array): boolean {
  if (a.length !== b.length) return false;
  let diff = 0;
  for (let i = 0; i < a.length; i++) {
    diff |= a[i] ^ b[i];
  }
  return diff === 0;
}

Performance Benchmarks

Apple M4 (2024), PBKDF2-SHA256, c=2^18 (262,144 iterations):

  • Synchronous: ~197ms
  • Asynchronous: ~200ms (slightly slower due to yielding)

Iteration count vs time (SHA-256):

  • 100,000: ~40ms
  • 250,000: ~100ms
  • 500,000: ~200ms
  • 1,000,000: ~400ms

PBKDF2 vs Alternatives

vs Scrypt:

  • PBKDF2: Less memory-hard, faster
  • Scrypt: Memory-hard, better against ASICs/GPUs
  • Use Scrypt when stronger protection needed

vs Argon2:

  • PBKDF2: Simple, widely supported
  • Argon2: Modern, memory-hard, configurable
  • Use Argon2 for new systems if available

vs bcrypt:

  • PBKDF2: Standard, flexible, any hash function
  • bcrypt: Fixed cost, limited input size
  • PBKDF2 generally preferred for new systems

When to use PBKDF2:

  • Need wide compatibility
  • FIPS compliance required
  • Simple key derivation
  • Resource-constrained environments

When to use alternatives:

  • Need maximum security (use Argon2)
  • Protect against GPU/ASIC attacks (use Scrypt or Argon2)
  • High-security password storage (use Argon2id)

Common Pitfalls

Pitfall 1: Low iteration count

// BAD: Too few iterations
const weak = pbkdf2(sha256, password, salt, { c: 1000 });

// GOOD: Sufficient iterations
const strong = pbkdf2(sha256, password, salt, { c: 100000 });

Pitfall 2: Non-random or short salt

// BAD: Static or predictable salt
const bad = pbkdf2(sha256, password, 'static-salt', { c: 100000 });

// GOOD: Random salt per password
const salt = randomBytes(16);
const good = pbkdf2(sha256, password, salt, { c: 100000 });

Pitfall 3: Timing attack in comparison

// BAD: Early exit reveals information
function badCompare(a: Uint8Array, b: Uint8Array): boolean {
  if (a.length !== b.length) return false;
  for (let i = 0; i < a.length; i++) {
    if (a[i] !== b[i]) return false; // Timing leak!
  }
  return true;
}

// GOOD: Constant-time comparison
function goodCompare(a: Uint8Array, b: Uint8Array): boolean {
  if (a.length !== b.length) return false;
  let diff = 0;
  for (let i = 0; i < a.length; i++) {
    diff |= a[i] ^ b[i];
  }
  return diff === 0;
}

Migration and Upgrades

interface StoredPassword {
  iterations: number;
  salt: string;
  hash: string;
}

async function verifyAndUpgrade(
  password: string,
  stored: StoredPassword,
  minIterations: number = 100000
): Promise<{ valid: boolean; upgraded?: StoredPassword }> {
  // Verify with stored parameters
  const salt = hexToBytes(stored.salt);
  const expectedHash = hexToBytes(stored.hash);

  const derivedHash = await pbkdf2Async(sha256, password, salt, {
    c: stored.iterations,
    dkLen: expectedHash.length
  });

  let diff = 0;
  for (let i = 0; i < derivedHash.length; i++) {
    diff |= derivedHash[i] ^ expectedHash[i];
  }

  const valid = diff === 0;

  // Upgrade if valid and iterations too low
  if (valid && stored.iterations < minIterations) {
    const newSalt = randomBytes(16);
    const newHash = await pbkdf2Async(sha256, password, newSalt, {
      c: minIterations,
      dkLen: 32
    });

    return {
      valid: true,
      upgraded: {
        iterations: minIterations,
        salt: bytesToHex(newSalt),
        hash: bytesToHex(newHash)
      }
    };
  }

  return { valid };
}

References

  • RFC 2898: PKCS #5: Password-Based Cryptography Specification Version 2.0
  • RFC 8018: PKCS #5: Password-Based Cryptography Specification Version 2.1
  • OWASP Password Storage Cheat Sheet: Current recommendations
  • NIST SP 800-132: Recommendation for Password-Based Key Derivation

Install with Tessl CLI

npx tessl i tessl/npm-noble--hashes@2.0.0

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