Audited & minimal 0-dependency JS implementation of SHA, RIPEMD, BLAKE, HMAC, HKDF, PBKDF & Scrypt
Scrypt is a memory-hard key derivation function designed to be resistant to hardware brute-force attacks (GPUs, ASICs, FPGAs). It requires significant amounts of memory in addition to computational power, making parallelized attacks much more expensive than CPU-bound functions like PBKDF2.
import { scrypt, scryptAsync } from '@noble/hashes/scrypt.js';Blocks execution until complete. Suitable for server environments where blocking is acceptable.
/**
* Scrypt key derivation (synchronous)
* @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.N - CPU/memory work factor (power of 2, 1 <= N <= 2^32)
* @param opts.r - Block size parameter (typically 8)
* @param opts.p - Parallelization factor (typically 1, JS doesn't benefit from p > 1)
* @param opts.dkLen - Desired key length in bytes (default: 32)
* @param opts.maxmem - Memory limit in bytes (default: 1GB + 1KB = 2^30 + 2^10)
* @returns Derived key
*/
function scrypt(
password: string | Uint8Array,
salt: string | Uint8Array,
opts: ScryptOpts
): Uint8Array;
/**
* Options for Scrypt
*/
interface ScryptOpts {
/** CPU/memory work factor (power of 2, 1 <= N <= 2^32) */
N: number;
/** Block size parameter (typically 8) */
r: number;
/** Parallelization factor (typically 1) */
p: number;
/** Desired output length in bytes (default: 32) */
dkLen?: number;
/** Memory limit in bytes (default: 2^30 + 2^10) */
maxmem?: number;
}Non-blocking, yields to event loop periodically. Recommended for browsers and UI applications. Supports progress callbacks.
/**
* Scrypt key derivation (asynchronous)
* @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.N - CPU/memory work factor (power of 2, 1 <= N <= 2^32)
* @param opts.r - Block size parameter (typically 8)
* @param opts.p - Parallelization factor (typically 1)
* @param opts.dkLen - Desired output length in bytes (default: 32)
* @param opts.maxmem - Memory limit in bytes (default: 2^30 + 2^10)
* @param opts.asyncTick - Max milliseconds before yielding to event loop (default: 10)
* @param opts.onProgress - Progress callback (receives 0.0 to 1.0)
* @returns Promise resolving to derived key
*/
function scryptAsync(
password: string | Uint8Array,
salt: string | Uint8Array,
opts: ScryptOpts
): Promise<Uint8Array>;
/**
* Options for asynchronous Scrypt
*/
interface ScryptOpts {
/** CPU/memory work factor (power of 2, 1 <= N <= 2^32) */
N: number;
/** Block size parameter (typically 8) */
r: number;
/** Parallelization factor (typically 1) */
p: number;
/** Desired output length in bytes (default: 32) */
dkLen?: number;
/** Memory limit in bytes (default: 2^30 + 2^10) */
maxmem?: number;
/** Max blocking time in ms before yielding (default: 10) */
asyncTick?: number;
/** Progress callback (0.0 to 1.0) */
onProgress?: (progress: number) => void;
}import { scrypt } from '@noble/hashes/scrypt.js';
import { randomBytes, bytesToHex, hexToBytes } from '@noble/hashes/utils.js';
// Generate random salt
const salt = randomBytes(16);
// Derive key from password
const key = scrypt('user-password', salt, {
N: 2 ** 16, // 65,536 iterations (~100ms)
r: 8,
p: 1,
dkLen: 32
});
// 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 = scrypt(password, salt, {
N: 2 ** 16,
r: 8,
p: 1,
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 { scryptAsync } from '@noble/hashes/scrypt.js';
import { randomBytes } from '@noble/hashes/utils.js';
async function hashPasswordWithProgress(
password: string,
onProgress: (percent: number) => void
): Promise<{ salt: Uint8Array; key: Uint8Array }> {
const salt = randomBytes(16);
const key = await scryptAsync(password, salt, {
N: 2 ** 18, // Higher security, ~400ms
r: 8,
p: 1,
dkLen: 32,
asyncTick: 10,
onProgress: (progress) => {
onProgress(Math.round(progress * 100));
}
});
return { salt, key };
}
// Usage
const result = await hashPasswordWithProgress('my-password', (percent) => {
console.log(`Progress: ${percent}%`);
});import { scryptAsync } from '@noble/hashes/scrypt.js';
import { randomBytes } from '@noble/hashes/utils.js';
interface EncryptionKey {
key: Uint8Array;
salt: Uint8Array;
params: {
N: number;
r: number;
p: number;
};
}
async function deriveEncryptionKey(
password: string
): Promise<EncryptionKey> {
const salt = randomBytes(32); // Larger salt for encryption keys
const params = { N: 2 ** 17, r: 8, p: 1 };
const key = await scryptAsync(password, salt, {
...params,
dkLen: 32
});
return { key, salt, params };
}
async function restoreEncryptionKey(
password: string,
salt: Uint8Array,
params: { N: number; r: number; p: number }
): Promise<Uint8Array> {
return scryptAsync(password, salt, {
...params,
dkLen: 32
});
}
// Usage
const { key, salt, params } = await deriveEncryptionKey('secret-password');
// ... encrypt data with key ...
// Later, restore key from password
const restoredKey = await restoreEncryptionKey('secret-password', salt, params);import { scryptAsync } from '@noble/hashes/scrypt.js';
import { hkdf } from '@noble/hashes/hkdf.js';
import { sha256 } from '@noble/hashes/sha2.js';
import { utf8ToBytes } from '@noble/hashes/utils.js';
async function deriveMultipleKeys(
password: string,
salt: Uint8Array
): Promise<{
encryptionKey: Uint8Array;
authKey: Uint8Array;
signingKey: Uint8Array;
}> {
// Step 1: Scrypt to strengthen password
const masterKey = await scryptAsync(password, salt, {
N: 2 ** 16,
r: 8,
p: 1,
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 { scryptAsync } from '@noble/hashes/scrypt.js';
import { randomBytes, bytesToHex, hexToBytes } from '@noble/hashes/utils.js';
interface PasswordHash {
algorithm: string;
N: number;
r: number;
p: number;
salt: string;
hash: string;
}
async function hashPassword(password: string): Promise<PasswordHash> {
const salt = randomBytes(16);
const N = 2 ** 16;
const r = 8;
const p = 1;
const hash = await scryptAsync(password, salt, {
N,
r,
p,
dkLen: 32
});
return {
algorithm: 'scrypt',
N,
r,
p,
salt: bytesToHex(salt),
hash: bytesToHex(hash)
};
}
async function verifyPassword(
password: string,
stored: PasswordHash
): Promise<boolean> {
if (stored.algorithm !== 'scrypt') {
throw new Error('Unsupported algorithm');
}
const salt = hexToBytes(stored.salt);
const expectedHash = hexToBytes(stored.hash);
const derivedHash = await scryptAsync(password, salt, {
N: stored.N,
r: stored.r,
p: stored.p,
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.N}$${hash.r}$${hash.p}$${hash.salt}$${hash.hash}`;
}
function parsePasswordHash(serialized: string): PasswordHash {
const [algorithm, N, r, p, salt, hash] = serialized.split('$');
return {
algorithm,
N: parseInt(N),
r: parseInt(r),
p: parseInt(p),
salt,
hash
};
}import { scryptAsync } from '@noble/hashes/scrypt.js';
import { randomBytes } from '@noble/hashes/utils.js';
async function calibrateScrypt(targetMs: number = 200): Promise<{
N: number;
r: number;
p: number;
}> {
const testSalt = randomBytes(16);
const testPassword = 'test-password';
const r = 8;
const p = 1;
// Test different N values
const nValues = [14, 15, 16, 17, 18, 19, 20];
let bestN = 16;
for (const nPower of nValues) {
const N = 2 ** nPower;
const start = performance.now();
try {
await scryptAsync(testPassword, testSalt, {
N,
r,
p,
dkLen: 32
});
const elapsed = performance.now() - start;
if (elapsed >= targetMs * 0.8 && elapsed <= targetMs * 1.2) {
bestN = nPower;
break;
} else if (elapsed < targetMs) {
bestN = nPower;
} else {
break;
}
} catch (e) {
// Memory limit exceeded
break;
}
}
return { N: 2 ** bestN, r, p };
}
// Usage: determine optimal parameters for this device
const params = await calibrateScrypt(200); // Target 200ms
console.log('Optimal parameters:', params);Scrypt is based on:
Memory requirement: N * r * 128 bytes
N (CPU/memory cost):
Common values:
N = 2^14 (16,384): ~16ms, 16MB RAM - minimum for interactive useN = 2^16 (65,536): ~100ms, 64MB RAM - recommended minimumN = 2^17 (131,072): ~200ms, 128MB RAM - good for password storageN = 2^18 (262,144): ~400ms, 256MB RAM - high securityN = 2^20 (1,048,576): ~1.5s, 1GB RAM - very high securityr (block size):
p (parallelization):
dkLen (output length):
maxmem (memory limit):
2^30 + 2^10 (1GB + 1KB)N * r * p * 128 + (128 * r * p)Apple M4 (2024) benchmarks with r=8, p=1:
| N pow | N value | Time | RAM |
|---|---|---|---|
| 14 | 16,384 | 0.02s | 16MB |
| 15 | 32,768 | 0.05s | 32MB |
| 16 | 65,536 | 0.1s | 64MB |
| 17 | 131,072 | 0.2s | 128MB |
| 18 | 262,144 | 0.4s | 256MB |
| 19 | 524,288 | 0.8s | 512MB |
| 20 | 1,048,576 | 1.5s | 1GB |
| 21 | 2,097,152 | 3.1s | 2GB |
| 22 | 4,194,304 | 6.2s | 4GB |
Mobile devices may be 2-4x slower.
Memory-Hardness: Requires O(N) memory, making parallel attacks expensive. An attacker needs N times more memory than a legitimate user.
Time-Memory Trade-off Resistance: Cannot significantly reduce memory usage by increasing computation time.
GPU/ASIC Resistance: Memory bandwidth requirements make specialized hardware less effective than for CPU-bound functions like PBKDF2.
Sequential Memory-Hard: ROMix phase must access memory in unpredictable sequential order.
vs PBKDF2:
vs Argon2:
vs bcrypt:
Single-threaded: p > 1 provides no benefit in JavaScript (unlike native implementations).
Memory: Large N values (> 2^20) may not work in all environments:
Performance: JavaScript is 1-2x slower than native implementations but attacker faces same penalty.
Salt Requirements:
// Good: Random, per-password salt
const salt = randomBytes(16);
// Bad: Fixed or predictable salt
const badSalt = 'fixed-salt'; // Don't do this!Parameter Storage:
// Store parameters with hash for upgradeability
interface StoredPassword {
N: number;
r: number;
p: number;
salt: Uint8Array;
hash: Uint8Array;
}Constant-Time Comparison:
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;
}DoS Protection:
// Set reasonable maxmem to prevent DoS
const key = scrypt(password, salt, {
N: 2 ** 16,
r: 8,
p: 1,
maxmem: 2 ** 27 // 128MB limit
});Pitfall 1: Too low N
// BAD: N too small, weak protection
const weak = scrypt(password, salt, { N: 2 ** 10, r: 8, p: 1 });
// GOOD: Adequate N value
const strong = scrypt(password, salt, { N: 2 ** 16, r: 8, p: 1 });Pitfall 2: Increasing p in JavaScript
// BAD: p > 1 doesn't help in JavaScript
const bad = scryptAsync(password, salt, { N: 2 ** 14, r: 8, p: 4 });
// GOOD: Increase N instead
const good = scryptAsync(password, salt, { N: 2 ** 16, r: 8, p: 1 });Pitfall 3: Not handling memory limit
// BAD: May exceed default maxmem
try {
const key = scrypt(password, salt, { N: 2 ** 21, r: 8, p: 1 });
} catch (e) {
// "Scrypt: maxmem must be at least ..."
}
// GOOD: Calculate and set maxmem
const N = 2 ** 21;
const r = 8;
const p = 1;
const maxmem = N * r * p * 128 + 128 * r * p;
const key = scrypt(password, salt, { N, r, p, maxmem });async function verifyAndUpgrade(
password: string,
stored: PasswordHash,
targetN: number = 2 ** 16
): Promise<{ valid: boolean; upgraded?: PasswordHash }> {
// Verify with stored parameters
const salt = hexToBytes(stored.salt);
const expectedHash = hexToBytes(stored.hash);
const derivedHash = await scryptAsync(password, salt, {
N: stored.N,
r: stored.r,
p: stored.p,
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 N too low
if (valid && stored.N < targetN) {
const newSalt = randomBytes(16);
const newHash = await scryptAsync(password, newSalt, {
N: targetN,
r: 8,
p: 1,
dkLen: 32
});
return {
valid: true,
upgraded: {
algorithm: 'scrypt',
N: targetN,
r: 8,
p: 1,
salt: bytesToHex(newSalt),
hash: bytesToHex(newHash)
}
};
}
return { valid };
}