Audited & minimal 0-dependency JS implementation of SHA, RIPEMD, BLAKE, HMAC, HKDF, PBKDF & Scrypt
Argon2 is the winner of the Password Hashing Competition (2015) and provides state-of-the-art password hashing. It offers three variants (argon2d, argon2i, argon2id) with configurable memory-hardness, making it resistant to GPU and ASIC attacks. Note: JavaScript implementation is 2-10x slower than native due to lack of efficient uint64 operations.
import {
argon2d, argon2i, argon2id,
argon2dAsync, argon2iAsync, argon2idAsync
} from '@noble/hashes/argon2.js';Blocks execution until complete. Suitable for server environments where blocking is acceptable.
/**
* Options for Argon2
*/
interface ArgonOpts {
/** Time cost / iterations (>= 1, recommended: 2-4 for interactive, 10+ for storage) */
t: number;
/** Memory cost in kibibytes (>= 8*p, recommended: 64MB-1GB) */
m: number;
/** Parallelization factor (1 <= p < 2^24, typically 1-4) */
p: number;
/** Algorithm version (default: 0x13 = 19, latest version) */
version?: number;
/** Optional key for keyed hashing */
key?: string | Uint8Array;
/** Optional personalization / associated data */
personalization?: string | Uint8Array;
/** Desired output length in bytes (default: 32, min: 4) */
dkLen?: number;
/** Memory limit in bytes (default: 2^32 - 1) */
maxmem?: number;
}
/**
* Argon2d - GPU-resistant variant (faster, vulnerable to side-channels)
* @param password - Password as string or Uint8Array
* @param salt - Salt as string or Uint8Array (MUST be at least 8 bytes; 16+ bytes recommended)
* @param opts - Options
* @param opts.t - Time cost / iteration count (>= 1)
* @param opts.m - Memory cost in kibibytes (>= 8 * p)
* @param opts.p - Parallelization parameter (1 <= p < 2^24)
* @param opts.version - Algorithm version (default: 0x13 = 19)
* @param opts.key - Optional key for keyed hashing
* @param opts.personalization - Optional application-specific data
* @param opts.dkLen - Desired output length in bytes (default: 32, minimum: 4)
* @param opts.maxmem - Memory limit in bytes (default: 2^32 - 1)
* @returns Derived key
*/
function argon2d(
password: string | Uint8Array,
salt: string | Uint8Array,
opts: ArgonOpts
): Uint8Array;
/**
* Argon2i - Side-channel resistant variant (slower, better against timing attacks)
* @param password - Password as string or Uint8Array
* @param salt - Salt as string or Uint8Array (MUST be at least 8 bytes; 16+ bytes recommended)
* @param opts - Options (same as argon2d)
* @returns Derived key
*/
function argon2i(
password: string | Uint8Array,
salt: string | Uint8Array,
opts: ArgonOpts
): Uint8Array;
/**
* Argon2id - Hybrid variant (recommended, combines benefits of d and i)
* @param password - Password as string or Uint8Array
* @param salt - Salt as string or Uint8Array (MUST be at least 8 bytes; 16+ bytes recommended)
* @param opts - Options (same as argon2d)
* @returns Derived key
*/
function argon2id(
password: string | Uint8Array,
salt: string | Uint8Array,
opts: ArgonOpts
): Uint8Array;Non-blocking, yields to event loop periodically. Recommended for browsers and UI applications. Supports progress callbacks.
/**
* Extended options for asynchronous Argon2
*/
interface ArgonOpts {
/** Time cost / iterations */
t: number;
/** Memory cost in kibibytes */
m: number;
/** Parallelization factor */
p: number;
/** Algorithm version */
version?: number;
/** Optional key */
key?: string | Uint8Array;
/** Optional personalization */
personalization?: string | Uint8Array;
/** Desired output length in bytes */
dkLen?: number;
/** Max blocking time in ms before yielding (default: 10) */
asyncTick?: number;
/** Memory limit */
maxmem?: number;
/** Progress callback (0.0 to 1.0) */
onProgress?: (progress: number) => void;
}
/**
* Argon2d (asynchronous)
* @param password - Password as string or Uint8Array
* @param salt - Salt as string or Uint8Array (MUST be at least 8 bytes; 16+ bytes recommended)
* @param opts - Options
* @param opts.t - Time cost / iteration count
* @param opts.m - Memory cost in kibibytes
* @param opts.p - Parallelization parameter
* @param opts.version - Algorithm version
* @param opts.key - Optional key
* @param opts.personalization - Optional personalization
* @param opts.dkLen - Desired output length in bytes
* @param opts.asyncTick - Max milliseconds before yielding (default: 10)
* @param opts.maxmem - Memory limit
* @param opts.onProgress - Progress callback (0.0 to 1.0)
* @returns Promise resolving to derived key
*/
function argon2dAsync(
password: string | Uint8Array,
salt: string | Uint8Array,
opts: ArgonOpts
): Promise<Uint8Array>;
/**
* Argon2i (asynchronous)
* Same parameters as argon2dAsync
* Salt MUST be at least 8 bytes; 16+ bytes recommended
*/
function argon2iAsync(
password: string | Uint8Array,
salt: string | Uint8Array,
opts: ArgonOpts
): Promise<Uint8Array>;
/**
* Argon2id (asynchronous, recommended)
* Same parameters as argon2dAsync
* Salt MUST be at least 8 bytes; 16+ bytes recommended
*/
function argon2idAsync(
password: string | Uint8Array,
salt: string | Uint8Array,
opts: ArgonOpts
): Promise<Uint8Array>;import { argon2id } from '@noble/hashes/argon2.js';
import { randomBytes, bytesToHex, hexToBytes } from '@noble/hashes/utils.js';
// Generate random salt (MUST be at least 8 bytes, 16+ bytes recommended)
const salt = randomBytes(16);
// Derive key from password using Argon2id (recommended variant)
const key = argon2id('user-password', salt, {
t: 2, // 2 iterations
m: 65536, // 64 MB memory
p: 1 // 1 thread (JS is single-threaded)
});
// 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 = argon2id(password, salt, {
t: 2,
m: 65536,
p: 1
});
// 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 { argon2idAsync } from '@noble/hashes/argon2.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 argon2idAsync(password, salt, {
t: 3, // 3 iterations
m: 262144, // 256 MB memory
p: 1,
asyncTick: 10,
onProgress: (progress) => {
onProgress(Math.round(progress * 100));
}
});
return { salt, key };
}
// Usage
const result = await hashPasswordWithProgress('my-password', (percent) => {
console.log(`Progress: ${percent}%`);
updateProgressBar(percent);
});import { argon2d, argon2i, argon2id } from '@noble/hashes/argon2.js';
const password = 'test-password';
const salt = new Uint8Array(16).fill(1);
const opts = { t: 2, m: 65536, p: 1 };
// Argon2d: Fastest, but vulnerable to side-channel attacks
// Use when: Speed is critical, attacker has no side-channel access
const hashD = argon2d(password, salt, opts);
// Argon2i: Slower, resistant to side-channel attacks
// Use when: Protection against timing attacks is critical
const hashI = argon2i(password, salt, opts);
// Argon2id: Hybrid, recommended for general use
// Use when: Want balance of speed and side-channel resistance
const hashId = argon2id(password, salt, opts);
// All produce different outputs due to different algorithmsimport { argon2idAsync } from '@noble/hashes/argon2.js';
import { randomBytes, bytesToHex, hexToBytes } from '@noble/hashes/utils.js';
interface PasswordHash {
algorithm: string;
version: number;
t: number;
m: number;
p: number;
salt: string;
hash: string;
}
async function hashPassword(password: string): Promise<PasswordHash> {
const salt = randomBytes(16);
const t = 2;
const m = 65536;
const p = 1;
const version = 0x13;
const hash = await argon2idAsync(password, salt, {
t,
m,
p,
version,
dkLen: 32
});
return {
algorithm: 'argon2id',
version,
t,
m,
p,
salt: bytesToHex(salt),
hash: bytesToHex(hash)
};
}
async function verifyPassword(
password: string,
stored: PasswordHash
): Promise<boolean> {
if (!stored.algorithm.startsWith('argon2')) {
throw new Error('Unsupported algorithm');
}
const salt = hexToBytes(stored.salt);
const expectedHash = hexToBytes(stored.hash);
// Select correct variant
let deriveFn;
switch (stored.algorithm) {
case 'argon2d':
deriveFn = argon2dAsync;
break;
case 'argon2i':
deriveFn = argon2iAsync;
break;
case 'argon2id':
deriveFn = argon2idAsync;
break;
default:
throw new Error(`Unknown variant: ${stored.algorithm}`);
}
const derivedHash = await deriveFn(password, salt, {
t: stored.t,
m: stored.m,
p: stored.p,
version: stored.version,
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 (PHC string format)
function serializePasswordHash(hash: PasswordHash): string {
return `$${hash.algorithm}$v=${hash.version}$m=${hash.m},t=${hash.t},p=${hash.p}$${hash.salt}$${hash.hash}`;
}
function parsePasswordHash(phc: string): PasswordHash {
const parts = phc.split('$').filter(p => p);
const [algorithm, versionStr, params, salt, hash] = parts;
const version = parseInt(versionStr.split('=')[1]);
const paramMap = Object.fromEntries(
params.split(',').map(p => p.split('='))
);
return {
algorithm,
version,
t: parseInt(paramMap.t),
m: parseInt(paramMap.m),
p: parseInt(paramMap.p),
salt,
hash
};
}import { argon2id } from '@noble/hashes/argon2.js';
import { randomBytes, utf8ToBytes } from '@noble/hashes/utils.js';
// Keyed hashing (pepper)
function hashWithPepper(
password: string,
salt: Uint8Array,
pepper: Uint8Array
): Uint8Array {
return argon2id(password, salt, {
t: 2,
m: 65536,
p: 1,
key: pepper // Additional secret, stored separately
});
}
// Personalization (domain separation)
function hashWithDomain(
password: string,
salt: Uint8Array,
domain: string
): Uint8Array {
return argon2id(password, salt, {
t: 2,
m: 65536,
p: 1,
personalization: utf8ToBytes(domain)
});
}
// Usage
const salt = randomBytes(16);
const pepper = randomBytes(32); // Stored in HSM or separate database
const hash1 = hashWithPepper('password', salt, pepper);
const hash2 = hashWithDomain('password', salt, 'email-auth-v1');Argon2d (Data-dependent):
Argon2i (Data-independent):
Argon2id (Hybrid, recommended):
Time cost (t):
Memory cost (m):
m >= 8 * pParallelization (p):
Version:
Output length (dkLen):
Apple M4 (2024) benchmarks for t: 1, m: 256MB, p: 1:
Memory usage formula:
Memory (bytes) = m (KiB) × 1024
Memory (MB) = m / 1024Common configurations:
t: 2, m: 65536 (64MB), p: 1: ~600mst: 3, m: 262144 (256MB), p: 1: ~2.9st: 1, m: 1048576 (1GB), p: 1: ~3.5sFor password storage:
m: 47104 KiB (46 MB) minimum, 19456 KiB (19 MB) if 47104 infeasiblet: 1 iterationp: 1 (or number of CPU cores)For FIPS compliance or if Argon2 unavailable, use PBKDF2-SHA256 with 600,000 iterations minimum.
JavaScript Argon2 is 2-10x slower than native implementations due to:
Impact: Attackers using native code have 2-10x advantage. For maximum security:
m and t parameters to compensateMemory-hardness: Requires configurable amount of memory (m parameter), making parallel attacks expensive.
Time-memory trade-off resistance: Cannot reduce memory significantly by increasing computation.
GPU/ASIC resistance: Memory bandwidth requirements and algorithm complexity make specialized hardware less effective.
Side-channel resistance (Argon2i/id): Memory access patterns don't depend on password.
Configurability: Adjustable security parameters for different threat models.
Interactive login (user waiting):
{ t: 2, m: 65536, p: 1 } // ~600ms, 64MBPassword storage (can take longer):
{ t: 3, m: 262144, p: 1 } // ~2.9s, 256MBHigh security (e.g., master password):
{ t: 10, m: 1048576, p: 1 } // ~35s, 1GBConstrained environment (IoT, mobile):
{ t: 3, m: 16384, p: 1 } // ~150ms, 16MBvs PBKDF2:
vs Scrypt:
vs bcrypt:
Pitfall 1: Too low parameters
// BAD: Weak protection
const weak = argon2id(password, salt, { t: 1, m: 8192, p: 1 });
// GOOD: Adequate protection
const strong = argon2id(password, salt, { t: 2, m: 65536, p: 1 });Pitfall 2: Using p > 1 in JavaScript
// BAD: No benefit in single-threaded JS
const bad = argon2id(password, salt, { t: 2, m: 65536, p: 4 });
// GOOD: p=1 for JavaScript
const good = argon2id(password, salt, { t: 2, m: 65536, p: 1 });Pitfall 3: Salt too short
// BAD: Less than 8 bytes will cause an error
const tooShortSalt = randomBytes(7); // ERROR: "salt" must be of length 8..4Gb
// ACCEPTABLE: 8-byte minimum, but weak
const minSalt = randomBytes(8);
// GOOD: 16+ bytes recommended for better security
const goodSalt = randomBytes(16);Pitfall 4: Not storing parameters
// BAD: Parameters hardcoded, can't upgrade
const hash = argon2id(password, salt, { t: 2, m: 65536, p: 1 });
// Lost the parameters!
// GOOD: Store parameters with hash
const params = { t: 2, m: 65536, p: 1 };
const hash = argon2id(password, salt, params);
// Store: { salt, hash, params }async function verifyAndUpgrade(
password: string,
stored: PasswordHash,
targetParams: { t: number; m: number; p: number } = { t: 3, m: 262144, p: 1 }
): Promise<{ valid: boolean; upgraded?: PasswordHash }> {
// Verify with stored parameters
const salt = hexToBytes(stored.salt);
const expectedHash = hexToBytes(stored.hash);
const derivedHash = await argon2idAsync(password, salt, {
t: stored.t,
m: stored.m,
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 parameters too low
if (valid && (stored.t < targetParams.t || stored.m < targetParams.m)) {
const newSalt = randomBytes(16);
const newHash = await argon2idAsync(password, newSalt, {
...targetParams,
dkLen: 32
});
return {
valid: true,
upgraded: {
algorithm: 'argon2id',
version: 0x13,
...targetParams,
salt: bytesToHex(newSalt),
hash: bytesToHex(newHash)
}
};
}
return { valid };
}