Command Palette

Search for a command to run...

EN·ES

Level 2 · 25 min

Password Hashing: bcrypt/argon2/scrypt

Password hashing is the seatbelt of authentication: ineffective until the DB leaks, and then it''s the only thing standing between attacker-acquired hashes and millions of plaintext passwords. Use a memory-hard KDF (argon2id, scrypt) or a tunable slow hash (bcrypt). Never use MD5, SHA-1, SHA-256, or any unsalted fast hash for passwords.

Why Fast Hashes Are Catastrophic

MD5, SHA-1, SHA-256 are designed to be fast — billions of hashes per second on a modern GPU. A consumer RTX 4090 computes ~25 GH/s of SHA-256 (25 billion per second). With a leaked DB of unsalted SHA-256 password hashes, every 8-character lowercase password is brute-forced in under 10 minutes; every 10-character mixed-case password in under a day; rockyou.txt''s 14M-entry wordlist is dispatched in seconds. Salts (per-user random bytes appended to the password before hashing) defeat rainbow tables but do not slow down brute force against a single user — the attacker simply hashes wordlist + salt for each user. The fix is making the hash function expensive: bcrypt''s cost factor 12 produces ~250ms per hash, reducing GPU throughput from billions/sec to ~3/sec — a 10-billion-fold slowdown. Argon2id additionally requires memory (64 MB+), which GPUs and ASICs handle poorly; scrypt is similar.

Concrete Cost Parameters

bcrypt: cost factor 12 in 2026 (raise as hardware improves; OWASP recommends checking the cost annually and bumping by 1 every 18-24 months). 12 = 2^12 = 4096 iterations. Bcrypt''s 72-byte input limit is a known limitation — pre-hash long inputs with HMAC-SHA-256 if you need to. Argon2id (preferred for new systems): m=64MB, t=3 iterations, p=4 parallelism. The OWASP Password Storage Cheat Sheet is the authoritative reference. scrypt: N=2^17, r=8, p=1 — heavier than Argon2id but supported by older platforms. PBKDF2: only acceptable in FIPS-constrained environments, with iterations in the millions for SHA-256 (FIPS 140-3 requires 1M+ in 2026). Aim for ~250-500ms per verification on production hardware — slower than that and login latency degrades; faster and the attacker''s job gets too easy. Time per hash should be benchmarked on the actual auth server hardware, not estimated from blog posts.

Salt, Pepper, Verification, and Rehash on Login

Salt: per-user random 16+ bytes, embedded in the hash output (bcrypt and argon2 do this automatically). Pepper: an optional server-side secret added to every password before hashing — kept outside the DB (in a vault or app config) so a DB-only leak still does not enable offline cracking. Pepper is defense in depth, not a substitute for a slow hash. Verification: use the library''s constant-time compare (bcrypt.compare, argon2.verify) — never strcmp the hashes (timing leak, though minor). Rehash on login: when verifying against a hash with a stale cost factor, transparently rehash the plaintext (which you have at login time) with the current parameters and update the DB. This lets you raise the cost factor over years without forcing password resets. Algorithm migration (e.g., bcrypt to argon2): same trick — store the algorithm prefix in the hash ($2b$ for bcrypt, $argon2id$ for argon2), branch on it at verify, and migrate users on next login.

Key Takeaways

  • bcrypt cost 12 (2026) or Argon2id m=64MB,t=3,p=4. Re-benchmark every year; raise the cost factor when hardware gets cheaper.
  • Salts defeat rainbow tables, slow KDFs defeat GPU/ASIC brute force. You need both — there is no one without the other.
  • Rehash on login lets you upgrade cost parameters and migrate algorithms transparently over time. Build it into the verify path from day one.

Code example

// Java with Spring Security 6+
// Configure: BCryptPasswordEncoder with cost 12
@Bean
PasswordEncoder passwordEncoder() {
  return new DelegatingPasswordEncoder("argon2", Map.of(
    "argon2", new Argon2PasswordEncoder(16, 32, 4, 65536, 3),  // saltLen, hashLen, p=4, m=64MB, t=3
    "bcrypt", new BCryptPasswordEncoder(12)
  ));
}

// Registration
String hash = encoder.encode(rawPassword);
// stored: $argon2id$v=19$m=65536,t=3,p=4$<salt>$<hash>

// Login — constant-time verify + rehash if stale
boolean ok = encoder.matches(rawPassword, storedHash);
if (ok) {
  if (encoder.upgradeEncoding(storedHash)) {
    // Cost factor or algorithm changed — rehash with current parameters
    String newHash = encoder.encode(rawPassword);
    userRepo.updatePasswordHash(userId, newHash);
  }
  // proceed with login
} else {
  // log failure (rate limit / lockout)
}

// Pepper (defense in depth) — HMAC the password with a server-side secret first
byte[] peppered = Mac.getInstance("HmacSHA256")
  .doFinal((PEPPER + rawPassword).getBytes(UTF_8));
String hash = encoder.encode(Base64.getEncoder().encodeToString(peppered));