Home › Security › Crypto primitives
algorithm low

Password hashing (Argon2id)

Problem A service that stores user passwords must make a leaked database expensive to crack, since attackers can test billions of guesses per second on GPUs against ordinary hashes.

Memory-hard, salted, deliberately slow hashing so stolen password databases can't be cracked cheaply on GPUs.

O(t·m) time and O(m) memory per hash; tuned to order of 10-100 ms and 19-64 MiB
passwordsargon2kdfmemory-hardbcryptscrypt
Overview

Fast hashes let an attacker with a leaked database test billions of guesses per second per GPU. Argon2id makes every guess fill and re-read a large block of memory, so attacks scale with RAM bandwidth, which GPUs can't cheaply multiply. Store a self-describing string (algorithm, parameters, salt, hash) and re-hash on login when parameters change. Web frameworks and password managers use it.

Key facts

Use when

  • Storing user passwords or deriving keys from passphrases.
  • Web applications: Django ships an Argon2PasswordHasher (docs); OWASP lists Argon2id first (cheat sheet).
  • Password managers: Bitwarden offers Argon2id as its key-derivation function (help).

Avoid when

  • The secret is already high-entropy (API keys, session tokens): one SHA-256 is enough.
  • You can avoid passwords entirely: passkeys.

Deep dive

How it works

In: password, 16-byte random salt, parameters m (memory), t (passes), p (lanes) · Out: 32-byte hash, stored as a PHC string · State: none in the function; the caller stores the string · Cost: O(t·m) time, m KiB of memory; order of 10-100 ms at 19-64 MiB

  1. H0 = BLAKE2b(parameters, password, salt, secret, associated data).
  2. Fill m KiB of memory as 1 KiB blocks, split into p lanes. Each block = compression G(previous block, a reference block).
  3. Reference index: Argon2i picks it independently of the password (side-channel safe); Argon2d picks it from data (GPU-resistant). Argon2id uses i for the first half of pass 1, d afterwards.
  4. Repeat t passes over memory; final block of each lane XORed, then hashed to the output length.

Storage format (PHC string):

$argon2id$v=19$m=19456,t=2,p=1$<salt b64>$<hash b64>

Code

Rust, argon2 = "0.5" (RustCrypto).

use argon2::{
    password_hash::{rand_core::OsRng, PasswordHash, PasswordHasher, PasswordVerifier, SaltString},
    Algorithm, Argon2, Params, Version,
};

fn hasher() -> Argon2<'static> {
    // m in KiB, t passes, p lanes, 32-byte output
    let params = Params::new(19 * 1024, 2, 1, Some(32)).expect("valid params");
    Argon2::new(Algorithm::Argon2id, Version::V0x13, params)
}

pub fn hash_password(password: &str) -> String {
    let salt = SaltString::generate(&mut OsRng); // 16 random bytes
    hasher()
        .hash_password(password.as_bytes(), &salt)
        .expect("hashing failed")
        .to_string() // PHC string: store this whole thing
}

pub enum Verify {
    Ok { rehash: Option<String> },
    Bad,
}

pub fn verify_password(password: &str, stored: &str) -> Verify {
    let Ok(parsed) = PasswordHash::new(stored) else { return Verify::Bad };
    // verify uses the params embedded in `stored`, so old hashes keep working
    if hasher().verify_password(password.as_bytes(), &parsed).is_err() {
        return Verify::Bad;
    }
    // upgrade on successful login if our current params differ
    let cur = hasher();
    let cur = cur.params();
    let stale = parsed.algorithm.as_str() != "argon2id"
        || Params::try_from(&parsed)
            .map(|p| (p.m_cost(), p.t_cost(), p.p_cost()) != (cur.m_cost(), cur.t_cost(), cur.p_cost()))
            .unwrap_or(true);
    Verify::Ok { rehash: stale.then(|| hash_password(password)) }
}

Run the hash on a blocking thread pool (tokio::task::spawn_blocking), not on an async executor thread.

Complexity and constants

  • Cost per hash ≈ t × m memory traffic. m = 19 MiB, t = 2 is order of 10-50 ms on a modern core; m = 64 MiB, t = 3 order of 100-200 ms.
  • Server capacity: one core does order of 20-100 logins/s at the OWASP minimum. Memory: m × concurrent logins (100 parallel × 64 MiB = 6.4 GiB). Cap concurrency with a semaphore.
  • Equivalent OWASP alternatives when memory is scarce: m = 12 MiB/t = 3, m = 9 MiB/t = 4, m = 7 MiB/t = 5 (all p = 1).
  • Fallbacks: scrypt N = 2^17, r = 8, p = 1 (128 MiB); bcrypt cost ≥ 10; PBKDF2-HMAC-SHA256 600,000 iterations (only when FIPS forces it).

Efficiency

  • Cost: deliberate: m = 19 MiB, t = 2 is order of 10-50 ms and 19 MiB per hash; m = 64 MiB, t = 3 order of 100-200 ms. Bound: memory bandwidth and latency (about t × m bytes touched); the goal is least defender CPU and RAM per unit of attacker cost.
  • Levers:
    • Memory over passes — OWASP equivalents 19 MiB/t=2 … 7 MiB/t=5 — take the largest m your peak login concurrency affords.
    • Lanes — p = 1 maximizes throughput under many logins; p > 1 cuts one hash's latency using cores (parallel feature of the argon2 crate) — few concurrent logins.
    • Semaphore plus blocking pool — caps RAM at m × running hashes and blunts login floods — every server.
    • Hash only when needed — re-hash on parameter change, reject oversized input, one SHA-256 for random tokens — always.
  • Naive → standard → state of the art:
    • Naive · salted SHA-256 · fast to crack, almost free to compute
    • Standard · Argon2id at OWASP parameters, order of 10-50 ms
    • State of the art · passkeys: no stored secret to hash; bcrypt cost ≥ 10 only where memory is scarce

Implementation notes

  • Tune: pick the largest m you can afford for peak login concurrency, then raise t until you hit the latency budget.
  • Pepper (optional): a server-side secret kept outside the DB (Argon2::new_with_secret, or HMAC the password first). Adds protection if only the DB leaks; you can't rotate it without re-hashing on login.
  • Normalize Unicode passwords (NFKC) before hashing so the same password typed on two keyboards matches.
  • Max password length: allow at least 64 characters, cap around 1-4 KiB to stop DoS via huge inputs.
  • Rate-limit login per account and per IP; hashing cost alone does not stop online guessing.
  • Check new passwords against known-breached lists (k-anonymity range API of Have I Been Pwned) instead of composition rules (NIST SP 800-63B).

Pitfalls

  • Using SHA-256/MD5 with a salt "because it's salted": salts stop rainbow tables, not GPU brute force.
  • bcrypt pre-hashing with raw binary SHA output: NUL bytes truncate the input. Base64-encode the pre-hash if you must.
  • Hashing on the async event loop: login spikes stall every request.
  • Tuning on a dev laptop, then deploying to a 1-vCPU container where each hash takes 5x longer.
  • Leaking account existence: return the same error and similar timing for unknown users (hash a dummy password).
  • Logging request bodies that contain passwords.

Variants and alternatives

bcryptfine for existing systems; 4 KiB memory, less GPU-resistant.
scryptmemory-hard, older; harder to tune (N, r, p interact).
PBKDF2only for FIPS 140 compliance; not memory-hard.
OPAQUEasymmetric PAKE (IETF CFRG); the server never sees the password.
Passkeysremove the stored secret problem entirely.

In production

  • Web frameworks: Django (Argon2 option, PBKDF2 default; docs), Spring Security Argon2PasswordEncoder, PHP password_hash(PASSWORD_ARGON2ID).
  • Password managers: Bitwarden (Argon2id KDF option; help).
  • Libraries: libsodium crypto_pwhash (Argon2id default).

Implementations

namelangrepoinstallverdictlicencehealthas_ofnotes
argon2RustRustCrypto/password-hashescargo add argon2————pure Rust, Argon2d/i/id, PHC strings, optional parallel
rust-argon2Rustsru-systems/rust-argon2cargo add rust-argon2————older standalone port, encoded-hash helpers
libsodiumCjedisct1/libsodiumbrew install libsodium————crypto_pwhash (Argon2id default); bindings for most languages
argon2-cffiPythonhynek/argon2-cffipip install argon2-cffi————bundles the C reference; check_needs_rehash()
golang.org/x/crypto/argon2Gogolang/cryptogo get golang.org/x/crypto/argon2————IDKey; derives raw bytes, you encode the PHC string

Sources

  • Biryukov, Dinu, Khovratovich, Josefsson, RFC 9106: Argon2 Memory-Hard Function for Password Hashing and Proof-of-Work Applications, 2021. RFC
  • Biryukov, Dinu, Khovratovich, Argon2: New Generation of Memory-Hard Functions for Password Hashing and Other Applications, IEEE EuroS&P 2016. paper · code
  • OWASP, Password Storage Cheat Sheet. doc
  • NIST, SP 800-63B: Digital Identity Guidelines, Authentication and Lifecycle Management. spec

Builds on

Used by

Systems (5):

Alternatives and related