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

argon2.mddocs/

Argon2: Modern Password Hashing

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.

Imports

import {
  argon2d, argon2i, argon2id,
  argon2dAsync, argon2iAsync, argon2idAsync
} from '@noble/hashes/argon2.js';

Capabilities

Synchronous Argon2

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;

Asynchronous Argon2

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

Usage Examples

Basic Password Hashing (Argon2id Recommended)

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

Asynchronous Password Hashing with Progress

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

Comparing Argon2 Variants

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 algorithms

Password Storage Format

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

Keyed Hashing and Personalization

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

Technical Details

Argon2 Variants

Argon2d (Data-dependent):

  • Memory access depends on password
  • Faster than Argon2i
  • Vulnerable to side-channel attacks (timing, cache)
  • Best for: Cryptocurrency mining, non-adversarial contexts

Argon2i (Data-independent):

  • Memory access independent of password
  • Resistant to side-channel attacks
  • Slower due to more computation
  • Best for: Password hashing where side-channels are a concern

Argon2id (Hybrid, recommended):

  • First half uses Argon2i, second half uses Argon2d
  • Balances speed and side-channel resistance
  • Recommended for password hashing (RFC 9106)
  • Best for: General-purpose password storage

Parameter Selection

Time cost (t):

  • Number of iterations through memory
  • Minimum: 1
  • Interactive use: 1-3
  • Password storage: 2-4 (min), 10+ (better)
  • Increases computation linearly

Memory cost (m):

  • Memory usage in kibibytes (KiB)
  • Minimum: 8 * p
  • Interactive use: 64MB (65536 KiB)
  • Password storage: 256MB-1GB (262144-1048576 KiB)
  • Most important security parameter
  • Constraint: m >= 8 * p

Parallelization (p):

  • Number of parallel threads
  • Range: 1 to 2^24 - 1
  • JavaScript: Always use p=1 (single-threaded)
  • Native: Can use p=4 or more on multi-core systems
  • Increases memory usage linearly

Version:

  • Current: 0x13 (19), from RFC 9106
  • Default in this implementation: 0x13
  • Always use latest version for new hashes

Output length (dkLen):

  • Minimum: 4 bytes
  • Default: 32 bytes (256 bits)
  • Common: 32 bytes (256 bits) or 64 bytes (512 bits)

Performance and Memory

Apple M4 (2024) benchmarks for t: 1, m: 256MB, p: 1:

  • Argon2id: ~2.8 seconds

Memory usage formula:

Memory (bytes) = m (KiB) × 1024
Memory (MB) = m / 1024

Common configurations:

  • t: 2, m: 65536 (64MB), p: 1: ~600ms
  • t: 3, m: 262144 (256MB), p: 1: ~2.9s
  • t: 1, m: 1048576 (1GB), p: 1: ~3.5s

OWASP Recommendations (2023)

For password storage:

  • Argon2id (recommended)
  • m: 47104 KiB (46 MB) minimum, 19456 KiB (19 MB) if 47104 infeasible
  • t: 1 iteration
  • p: 1 (or number of CPU cores)

For FIPS compliance or if Argon2 unavailable, use PBKDF2-SHA256 with 600,000 iterations minimum.

JavaScript Performance Warning

JavaScript Argon2 is 2-10x slower than native implementations due to:

  • Lack of native uint64 operations
  • No SIMD instructions
  • Single-threaded execution (p > 1 has no benefit)
  • JIT compilation overhead

Impact: Attackers using native code have 2-10x advantage. For maximum security:

  • Use native Argon2 if possible (Node.js addon, WASM)
  • Increase m and t parameters to compensate
  • Consider hybrid approach: Argon2 server-side, PBKDF2 client-side

Security Properties

Memory-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.

Common Configurations

Interactive login (user waiting):

{ t: 2, m: 65536, p: 1 }  // ~600ms, 64MB

Password storage (can take longer):

{ t: 3, m: 262144, p: 1 }  // ~2.9s, 256MB

High security (e.g., master password):

{ t: 10, m: 1048576, p: 1 }  // ~35s, 1GB

Constrained environment (IoT, mobile):

{ t: 3, m: 16384, p: 1 }  // ~150ms, 16MB

Argon2 vs Other KDFs

vs PBKDF2:

  • Argon2: Memory-hard, better protection
  • PBKDF2: CPU-only, faster, more compatible
  • Use Argon2: Modern systems, high security
  • Use PBKDF2: Wide compatibility, FIPS compliance

vs Scrypt:

  • Argon2: More configurable, better side-channel resistance
  • Scrypt: Simpler, established
  • Use Argon2: New systems, maximum security
  • Use Scrypt: Good balance, RFC standard

vs bcrypt:

  • Argon2: Configurable, no password length limit
  • bcrypt: Simple, 72-byte password limit
  • Use Argon2: Modern applications, long passwords
  • Use bcrypt: Simple legacy compatibility

Common Pitfalls

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 }

Migration and Upgrades

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

References

  • RFC 9106: Argon2 Memory-Hard Function for Password Hashing and Proof-of-Work Applications
  • Argon2 Paper: Original specification
  • Password Hashing Competition: Background
  • OWASP Password Storage Cheat Sheet: Current best practices

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