DevToys Web Pro iconDevToys Web ProBlogg
Översatt med LocalePack logoLocalePack
Betygsätt oss:
Prova webbläsartillägget:
← Back to Blog

scrypt Guide: Memory-Hard Password Hashing and Key Derivation

9 min read

scrypt was designed in 2009 by Colin Percival to be simultaneously CPU-hard and memory-hard — making it expensive to attack with the dedicated hardware (ASICs and GPU farms) that makes simpler algorithms cheap to crack at scale. Use the scrypt Generator to experiment with parameters and observe the timing impact in your browser. For the full context on password hashing philosophy, see Hashes, Encryption, and Password Hashing.

Why Memory-Hard KDFs Exist

The fundamental problem with CPU-only password hashing algorithms is economics. An attacker running bcrypt on a specialized ASIC chip can do so at a fraction of the cost and power consumption of a defender running the same algorithm on a general-purpose server. ASIC manufacturers optimize silicon for exactly one job — computing the target function as fast and cheaply as possible.

Memory-hard functions attack this asymmetry at the hardware level. To compute a memory-hard hash, the algorithm must read and write a large working set of RAM repeatedly and in a pattern that cannot be parallelized cheaply. DRAM access latency (~100 ns) is a physical constant that cannot be designed away. An ASIC running a memory-hard algorithm needs the same expensive DRAM as a commodity server — so the cost-per-hash advantage of custom hardware collapses.

scrypt quantifies this directly: its memory requirement is approximately 128 × N × r bytes. With N = 2¹⁷ (131,072) and r = 8, scrypt requires roughly 128 MB of RAM per hash. A GPU with 8 GB of VRAM can run at most ~64 parallel scrypt instances — compared to millions of parallel bcrypt instances on the same card.

scrypt Parameters: N, r, and p

scrypt exposes three tuning parameters. Understanding what each one controls is essential to choosing safe values:

  • N — CPU/memory cost factor. This is the main work factor. It must be a power of 2. Doubling N doubles both the time and memory required. N = 2¹⁷ = 131,072 is the recommended baseline for 2025–2026 hardware.
  • r — block size. Controls the size of the mixing block in bytes: block = 128 × r bytes. Increasing r increases memory bandwidth usage and total memory proportionally. The default r = 8 is appropriate for most hardware; changing it rarely provides meaningful benefit without also adjusting N.
  • p — parallelization factor. Allows p independent scrypt computations to run in parallel. Increasing p multiplies the CPU cost without increasing per-thread memory. For password hashing, p = 1 is standard; higher p is used when you want more CPU cost without more memory.

Memory used per hash: 128 × N × r bytes. At N = 2¹⁷ and r = 8: 128 × 131,072 × 8 = 134,217,728 bytes = 128 MB.

Parameter Tuning by Use Case

Use caseNrpMemoryApprox. time
Baseline / default2¹⁷ = 131,07281~128 MB~1 s
Web login (high traffic)2¹⁵ = 32,76881~32 MB~250 ms
High-security / offline keys2²⁰ = 1,048,57681~1 GB~8 s
Embedded / memory-constrained2¹⁴ = 16,38481~16 MB~125 ms

The OWASP guidance is to target ~250 ms per hash on your production hardware. Benchmark on actual servers before committing to parameters — cloud instance performance varies significantly between instance types and generations. N = 2¹⁵ with r = 8 and p = 1 is a reasonable starting point for interactive web logins.

scrypt vs Argon2id

Argon2id won the Password Hashing Competition in 2015 and is OWASP's first choice for new systems. Both Argon2id and scrypt are memory-hard, but they differ in several practical ways:

PropertyscryptArgon2id
StandardizedRFC 7914 (2016)RFC 9106 (2021)
Memory parameterN × r (interlinked)m (independent of time cost)
Time parameterN (doubles memory too)t (iterations, independent)
Side-channel resistanceModerateStrong (Argon2id hybrid mode)
Library supportWide (Node built-in, Python 3.6+)Growing (requires native bindings on some platforms)
Parameter independenceN controls both time and memoryt and m tuned separately

Argon2id's independent time and memory parameters make it easier to tune: you can increase iterations without increasing RAM use, or vice versa. scrypt's N factor controls both simultaneously, which can make fine-grained tuning awkward.

Argon2id also has stronger theoretical side-channel resistance in its hybrid mode (combining data-independent and data-dependent memory access patterns). For new projects with no compatibility constraints, prefer Argon2id. Use scrypt when your platform has built-in support (Node.js crypto.scrypt, Python hashlib.scrypt) and Argon2id would require adding a native dependency.

scrypt vs bcrypt

The key difference is the memory dimension. bcrypt uses a fixed ~4 KB memory footprint — small enough that a modern GPU can run thousands of bcrypt instances in parallel even at high cost factors. scrypt's memory requirement scales with N, making parallel GPU attacks proportionally more expensive as N increases.

Propertybcryptscrypt
Memory per hash~4 KB (fixed)128 × N × r bytes (configurable)
GPU resistanceModerate (CPU-bound only)Strong (memory-bound)
ASIC resistanceLowHigh
Password length limit72 bytes (silent truncation)No practical limit
Built-in to standard libsNo (requires package)Yes (Node crypto, Python hashlib)
OWASP rankingThird choiceSecond choice

Use bcrypt when memory budget is severely constrained — for example, on embedded systems or environments where allocating 32–128 MB per authentication attempt is not feasible. For all other new systems, scrypt is the stronger choice between the two. See the bcrypt Guide for a deeper look at bcrypt's cost factor model and the bcrypt Generator for side-by-side comparison.

Uses Beyond Password Storage

scrypt is a key derivation function (KDF), not just a password hasher. Its output is a raw key of configurable length — which makes it useful anywhere you need to derive cryptographic key material from a low-entropy secret (a passphrase or PIN).

Common use cases beyond password verification:

  • Encrypting data with a passphrase. Derive a 256-bit AES key from a user-supplied passphrase using scrypt. The derived key encrypts the actual data; the passphrase is never stored. This is the pattern used by many encrypted backup tools and password managers.
  • Cryptocurrency proof-of-work. Litecoin and Dogecoin use scrypt as their proof-of-work function. Note that the workload is very different from password hashing: PoW runs at low N (N = 1,024) for fast iteration during mining, which is not the same as password-strength parameters. Do not confuse mining-profile scrypt with password-hashing scrypt.
  • Disk encryption key derivation. Some full-disk encryption tools use scrypt to stretch a passphrase into the master key that unlocks the volume.

The output length is configurable (the dkLen parameter). For a 256-bit AES key, use dkLen = 32. For a 512-bit key, use dkLen = 64.

Code Examples

Node.js (built-in crypto.scrypt)

import { scrypt, randomBytes, timingSafeEqual } from 'crypto';
import { promisify } from 'util';

const scryptAsync = promisify(scrypt);

// Parameters: N=2^17, r=8, p=1, output 64 bytes
const PARAMS = { N: 131072, r: 8, p: 1 };
const KEY_LEN = 64;

async function hashPassword(password: string): Promise<string> {
  const salt = randomBytes(16);
  const derived = await scryptAsync(password, salt, KEY_LEN, PARAMS) as Buffer;
  // Store as: salt (hex) + ':' + hash (hex)
  return salt.toString('hex') + ':' + derived.toString('hex');
}

async function verifyPassword(password: string, stored: string): Promise<boolean> {
  const [saltHex, hashHex] = stored.split(':');
  const salt = Buffer.from(saltHex, 'hex');
  const storedHash = Buffer.from(hashHex, 'hex');
  const derived = await scryptAsync(password, salt, KEY_LEN, PARAMS) as Buffer;
  // timingSafeEqual prevents timing attacks
  return timingSafeEqual(derived, storedHash);
}

Node's crypto.scrypt has been available since Node 10 with no additional packages required. The N, r, and p options map directly to scrypt's parameters.

Python (hashlib.scrypt / passlib)

import hashlib
import os
import hmac

# Parameters: N=2^17, r=8, p=1
N, r, p = 131072, 8, 1
KEY_LEN = 64

def hash_password(password: str) -> str:
    salt = os.urandom(16)
    dk = hashlib.scrypt(
        password.encode('utf-8'),
        salt=salt,
        n=N, r=r, p=p,
        dklen=KEY_LEN
    )
    return salt.hex() + ':' + dk.hex()

def verify_password(password: str, stored: str) -> bool:
    salt_hex, hash_hex = stored.split(':')
    salt = bytes.fromhex(salt_hex)
    stored_hash = bytes.fromhex(hash_hex)
    dk = hashlib.scrypt(
        password.encode('utf-8'),
        salt=salt,
        n=N, r=r, p=p,
        dklen=KEY_LEN
    )
    # Use hmac.compare_digest for constant-time comparison
    return hmac.compare_digest(dk, stored_hash)
# Using passlib for higher-level API
pip install passlib
from passlib.hash import scrypt

# Hash with passlib (handles salt, serialization, verification)
hashed = scrypt.using(rounds=17, block_size=8, parallelism=1).hash("password")

# Verify
is_valid = scrypt.verify("password", hashed)  # True

Rust (scrypt crate)

# Cargo.toml
scrypt = "0.11"
password-hash = "0.5"
rand_core = { version = "0.6", features = ["std"] }
use scrypt::{
    password_hash::{
        rand_core::OsRng,
        PasswordHash, PasswordHasher, PasswordVerifier, SaltString,
    },
    Params, Scrypt,
};

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let password = b"my_password";

    // N=2^17, r=8, p=1
    let params = Params::new(17, 8, 1, 64)?;
    let salt = SaltString::generate(&mut OsRng);

    // Hash
    let hash = Scrypt
        .hash_password_customized(password, None, None, params, &salt)?
        .to_string();

    // Verify
    let parsed = PasswordHash::new(&hash)?;
    let is_valid = Scrypt.verify_password(password, &parsed).is_ok();
    println!("Valid: {}", is_valid);

    Ok(())
}

OWASP Recommendation

OWASP's Password Storage Cheat Sheet ranks password hashing algorithms as follows for new projects:

  1. Argon2id — first choice; memory-hard, best side-channel resistance, independent time and memory parameters. Minimum: m=19456 (19 MB), t=2, p=1.
  2. scrypt — second choice; memory-hard, widely available in standard libraries. Minimum: N=2¹⁷, r=8, p=1.
  3. bcrypt — third choice; CPU-bound only, fixed memory. Minimum cost factor 10, target 12.
  4. PBKDF2 — acceptable only when FIPS compliance is required; not memory-hard.

If you are starting a new project today with no platform constraints, use Argon2id. If your platform provides scrypt natively (Node.js, Python) and Argon2id requires a native binary dependency, scrypt is a sound second choice. See bcrypt Guide for when bcrypt is the right pick.

Common Pitfalls

  • Library defaults are often too weak. Many libraries ship with N = 2¹⁴ or N = 16,384 as the default — a value that dates to 2009 hardware. Always set N explicitly. Check the default before trusting it.
  • Memory DoS on shared hosting. N = 2¹⁷ allocates ~128 MB per hash operation. On a shared host or serverless function with 128 MB total memory, this will exhaust available RAM and crash the process. Use N = 2¹⁵ (~32 MB) in memory-constrained environments, or gate hashing behind a queue.
  • Mismatched parameters break verification. The stored salt and hash encode no parameter metadata (unlike bcrypt's self-describing hash string). If you change N, r, or p across deployments, existing hashes computed with old parameters will fail verification. Store parameters alongside the hash, or use a library like passlib that serializes them in the hash string.
  • Confusing mining scrypt with password scrypt. Cryptocurrency mining profiles use N = 1,024 for throughput. This is not a safe password-hashing parameter. Always use password-appropriate N values (2¹⁵ minimum, 2¹⁷ preferred).
  • Not using timing-safe comparison. Always use timingSafeEqual (Node), hmac.compare_digest (Python), or equivalent when comparing derived keys. Naive byte comparison leaks timing information.

Hash and verify passwords directly in your browser with no data leaving your machine using the scrypt Generator. Adjust N, r, and p and observe the timing and memory impact in real time. For side-by-side comparison with bcrypt, see the bcrypt Generator.