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

scrypt.mddocs/

Scrypt: Memory-Hard Key Derivation Function

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.

Imports

import { scrypt, scryptAsync } from '@noble/hashes/scrypt.js';

Capabilities

Synchronous Scrypt

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

Asynchronous Scrypt

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

Usage Examples

Basic Password Hashing

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

Asynchronous Password Hashing with Progress

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

Encryption Key from Password

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

Multiple Keys with HKDF

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

Password Storage Format

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

Adaptive Parameter Selection

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

Technical Details

Scrypt Algorithm

Scrypt is based on:

  1. PBKDF2: Used for initial key derivation
  2. ROMix: Memory-hard mixing function
  3. Salsa20/8: Core cryptographic primitive

Memory requirement: N * r * 128 bytes

Parameter Selection

N (CPU/memory cost):

  • Must be power of 2
  • Higher values increase both CPU and memory requirements
  • Most important security parameter

Common values:

  • N = 2^14 (16,384): ~16ms, 16MB RAM - minimum for interactive use
  • N = 2^16 (65,536): ~100ms, 64MB RAM - recommended minimum
  • N = 2^17 (131,072): ~200ms, 128MB RAM - good for password storage
  • N = 2^18 (262,144): ~400ms, 256MB RAM - high security
  • N = 2^20 (1,048,576): ~1.5s, 1GB RAM - very high security

r (block size):

  • Typically 8 (default)
  • Higher values increase memory and CPU usage
  • Usually not changed
  • Memory scales linearly with r

p (parallelization):

  • Typically 1 for JavaScript (single-threaded)
  • Higher values would allow parallel processing on multiple cores
  • JavaScript implementation doesn't benefit from p > 1
  • Increases memory linearly

dkLen (output length):

  • Default: 32 bytes (256 bits)
  • Can be any length
  • Common: 32 or 64 bytes

maxmem (memory limit):

  • Default: 2^30 + 2^10 (1GB + 1KB)
  • Formula: N * r * p * 128 + (128 * r * p)
  • Prevents DoS attacks
  • Must be adjusted for N > 2^20

Time and Memory Table

Apple M4 (2024) benchmarks with r=8, p=1:

N powN valueTimeRAM
1416,3840.02s16MB
1532,7680.05s32MB
1665,5360.1s64MB
17131,0720.2s128MB
18262,1440.4s256MB
19524,2880.8s512MB
201,048,5761.5s1GB
212,097,1523.1s2GB
224,194,3046.2s4GB

Mobile devices may be 2-4x slower.

Security Properties

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.

Scrypt vs Other KDFs

vs PBKDF2:

  • Scrypt: Memory-hard, better against GPU/ASIC
  • PBKDF2: CPU-bound, faster, wider support
  • Use Scrypt: Higher security requirements, modern systems
  • Use PBKDF2: Wide compatibility, resource-constrained

vs Argon2:

  • Scrypt: Simpler, well-established (RFC 7914)
  • Argon2: Configurable memory-hardness, more modern
  • Use Scrypt: Good balance of security and simplicity
  • Use Argon2: Need maximum security, fine-tuned control

vs bcrypt:

  • Scrypt: More memory-hard, flexible parameters
  • bcrypt: Simpler, fixed algorithm, max 72-byte passwords
  • Use Scrypt: Modern applications, long passwords
  • Use bcrypt: Simple fixed-cost hashing

JavaScript Limitations

Single-threaded: p > 1 provides no benefit in JavaScript (unlike native implementations).

Memory: Large N values (> 2^20) may not work in all environments:

  • Node.js: Generally supports large N
  • Browsers: May have lower memory limits
  • Mobile: More constrained

Performance: JavaScript is 1-2x slower than native implementations but attacker faces same penalty.

Security Best Practices

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

Common Pitfalls

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

Migration and Upgrades

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

References

  • RFC 7914: The scrypt Password-Based Key Derivation Function
  • Scrypt Website: Original implementation and paper
  • Scrypt Parameters Blog: Practical parameter selection guide

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