Password Storage (bcrypt & Argon2)

Why the defence is deliberate slowness and memory cost, what salt and pepper each actually stop, and the current parameter recommendations for Argon2id, bcrypt, scrypt and PBKDF2.

Password hashing inverts the usual goal. Everywhere else in this domain, faster is better; here speed is the vulnerability. Users pick low-entropy secrets — a realistic password carries perhaps 20–40 bits — so an attacker who steals the database does not need to break the hash, only to guess. The entire defence is making each guess expensive enough that guessing at scale stops paying.

The numbers are stark. A modern GPU rig computes billions of SHA-256 hashes per second; every 8-character password over a full character set falls in hours. Against correctly-parameterized Argon2id the same rig manages thousands per second — six to seven orders of magnitude, achieved entirely by choosing the right function.

Salt, and what it actually stops #

A salt is a unique, random, per-user value stored alongside the hash (not secret).

Attack Without salt With unique salt
Precomputed rainbow tables one table breaks every database useless — table would be per-salt
Cracking many users at once one guess tested against all rows simultaneously each guess must be recomputed per user
Identical passwords visible equal hashes reveal equal passwords hashes differ

Salts must be unique per password (not per application) and come from the CSPRNG, ≥16 bytes. Every modern password hash generates and encodes the salt for you inside its output string — one of several reasons not to hand-roll this.

A pepper is a secret value mixed in, stored outside the database — in a KMS, an HSM, or app config. It is defence in depth for the specific scenario where the database leaks but the application secret does not (SQL injection, stolen backup). Apply it as HMAC(pepper, password) before hashing, or use a keyed hash. Peppers cannot be rotated without rehashing, so plan for versioning.

The algorithms #

Argon2id — winner of the 2015 Password Hashing Competition, specified in RFC 9106. Tunable independently on memory, time (iterations), and parallelism. The id variant is a hybrid: the first pass is data-independent (resisting side-channel leakage of access patterns), later passes are data-dependent (resisting time-memory tradeoffs). This is the default recommendation.

scrypt — the original memory-hard design (2009). Sound, widely available, and still fine; parameter interaction (N, r, p) is less intuitive than Argon2's.

bcrypt — 1999, built on Blowfish's deliberately expensive key schedule made iterable via a cost parameter. Battle-tested and everywhere, but it uses only 4 KB of memory, so it is far less GPU-resistant than the memory-hard functions.

PBKDF2 — iterated HMAC. The weakest of the four: no memory hardness at all, so it maps almost perfectly onto GPUs and ASICs. Its reason to exist is FIPS-140 compliance.

Current parameters #

Per the OWASP Password Storage Cheat Sheet:

Algorithm Recommended minimum
Argon2id 19 MiB memory, 2 iterations, 1 parallelism (or 46 MiB, 1 iteration, 1 parallelism)
scrypt N=217N = 2^{17}, r=8r = 8, p=1p = 1
bcrypt work factor ≥ 10 (12 is a common stronger choice); 72-byte input limit
PBKDF2 ≥ 600,000 iterations with HMAC-SHA-256 (FIPS only)

These are floors. The practical method is to tune upward to the largest cost your login path tolerates — typically targeting 250–500 ms per verification on production hardware — and revisit as hardware improves. Store the parameters in the hash string (all four formats do) so you can raise them later.

Operating it #

  • Rehash on login. When a user authenticates successfully and their stored hash uses outdated parameters, recompute with current ones — you have the plaintext exactly then and never again.
  • Migrating a legacy fast-hash database. Wrap rather than wait: store argon2id(sha256(old_password)) for every row immediately, mark it wrapped, and unwrap on next successful login. Do not leave weak hashes sitting while you wait for users to return.
  • Constant-time verification — use the library's verify function; see MAC comparison.
  • Don't leak account existence. Login failure must look and take the same time whether the user exists or not; hash a dummy value on the miss path.
  • Rate-limit and lock out on the authentication endpoint. Password hashing protects the stolen database; it does nothing against online guessing.

Sources

  1. docsOWASP Password Storage Cheat Sheet
  2. docsRFC 9106 — Argon2 Memory-Hard Function for Password Hashing
  3. paperA Future-Adaptable Password Scheme (Provos & Mazières, 1999)
  4. docsNIST SP 800-63B — Digital Identity Guidelines: Authentication

Searches titles, aliases, headings, summaries, and full text.↑↓ move ↵ open esc close