Audited & minimal 0-dependency JS implementation of SHA, RIPEMD, BLAKE, HMAC, HKDF, PBKDF & Scrypt
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.
import { pbkdf2, pbkdf2Async } from '@noble/hashes/pbkdf2.js';
import { sha256, sha512 } from '@noble/hashes/sha2.js';
import { sha3_256 } from '@noble/hashes/sha3.js';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;
}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;
}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;
}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;
}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);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 };
}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
};
}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);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>Iteration Count (c):
Time on Apple M4 (2024):
Salt Length:
Output Length (dkLen):
SHA-256 (recommended):
SHA-512:
SHA-3 variants:
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 });Salt Requirements:
Iteration Count:
Password Input:
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;
}Apple M4 (2024), PBKDF2-SHA256, c=2^18 (262,144 iterations):
Iteration count vs time (SHA-256):
vs Scrypt:
vs Argon2:
vs bcrypt:
When to use PBKDF2:
When to use alternatives:
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;
}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 };
}