Topic 09

Password Storage Done Right

Passwords

Passwords must be stored so that a stolen database does not hand the attacker the passwords — which means never storing them recoverably, and never with a fast hash. The right tool is a slow, salted password-hashing function: argon2, bcrypt, or scrypt, tuned so each guess is expensive.

This is where the beginner course's "hashing versus encryption" becomes a concrete, correct implementation for Meridian's login. Get it right and a database dump yields hashes that cost months of compute to crack; get it wrong and the whole customer base is exposed the moment the table leaks.

Why Not Encryption, and Why Not a Fast Hash

Encryption is the wrong tool because it is reversible: the key exists somewhere to be stolen, and "encrypted passwords" is almost always a design mistake. A fast general-purpose hash like SHA-256 is also wrong, for the opposite reason — it is too fast. A modern GPU tries billions of SHA-256 guesses per second, so a stolen table of fast hashes is cracked at scale. Password storage needs a function that is deliberately, tunably slow.

Salting Defeats Precomputation

A salt is a unique random value stored alongside each password hash. Because every password gets a different salt, two users with the same password get different hashes, and an attacker cannot use a precomputed rainbow table — they must attack each hash individually. The salt is not secret; it just has to be unique per password, and the library generates it for you.

Slow Hashes and Work Factors

The slowness is tunable through a work factor: bcrypt's cost, scrypt's memory, argon2's memory-and-time parameters. You set it so a single verification takes a noticeable fraction of a second on your hardware, which is invisible to a legitimate login but makes large-scale guessing brutally expensive. As hardware improves, you raise the factor — argon2id is the current first choice, with bcrypt the safe, battle-tested incumbent.

Hash a Meridian password with argon2id
# register: hash with a per-user random salt and tuned parameters (library picks a safe salt)
# python, using the argon2-cffi library
from argon2 import PasswordHasher
ph = PasswordHasher(time_cost=3, memory_cost=65536, parallelism=4)
stored = ph.hash(user_password)   # store this string; it encodes salt + params + hash

# login: verify in constant time; raises on mismatch
ph.verify(stored, submitted_password)

The stored string encodes the algorithm, the parameters, and the per-user salt along with the hash, so verification needs nothing else. A database dump yields only these expensive-to-crack strings — no plaintext, no reversible ciphertext, and no shortcut past the tuned work factor. When you later raise the parameters, you re-hash each password on its next successful login.

Peppering as Defense in Depth

An optional pepper is a secret value kept outside the database — in a KMS — and mixed into every hash. Because it is not in the table, a pure database leak (SQL injection, a stolen backup) yields hashes an attacker cannot even begin to crack without also breaching the KMS. A pepper complements the salt and the slow hash; it never replaces them.

argon2 vs bcrypt vs scrypt vs PBKDF2

argon2id — the current recommendation; memory-hard, so it resists GPU and ASIC cracking. The default for new systems.

bcrypt — battle-tested and a fine choice, with a roughly 72-byte input limit to keep in mind.

scrypt — memory-hard and good where available.

PBKDF2 — acceptable only when a certified or FIPS primitive is required; it is fast on GPUs, so it needs very high iteration counts. Never a plain SHA or MD5.

Common Mistakes
  • Storing passwords encrypted or, worse, in plaintext — the key or the plaintext is right there to steal, and "encrypted passwords" is almost always the wrong design.
  • Using a fast general-purpose hash (SHA-256, MD5) for passwords, so a stolen table is cracked at billions of guesses per second.
  • Reusing one salt for all users or omitting the salt, which makes identical passwords collide and rainbow tables work again.
  • Never raising the work factor, so parameters chosen years ago are now cheap to brute-force on current hardware.
  • Rolling a custom password scheme instead of a vetted library, reintroducing long-solved flaws.
Best Practices
  • Hash passwords with argon2id — or bcrypt or scrypt — tuned so a single verification takes a noticeable fraction of a second on your hardware.
  • Use a unique cryptographically random salt per password, stored with the hash; let the library generate and encode it.
  • Consider a KMS-held pepper as an extra layer for high-value systems, kept strictly outside the password database.
  • Verify in constant time and plan to re-hash on the next login when you raise the work factor or migrate algorithms.
  • Reject known-breached passwords at set time so users do not pick credentials already in a dump (Chapter 6).
Comparable toolsRecommended argon2id · bcrypt · scryptConstrained PBKDF2 (FIPS, high iterations)Libraries libsodium crypto_pwhash · passlib · argon2-cffi

Knowledge Check

Why is a fast hash like SHA-256 the wrong choice for storing passwords?

  • Its speed allows billions of guesses a second
  • It is reversible, so the plaintext can be recovered directly
  • It cannot accept a salt as one of its inputs at all
  • It produces a digest that is too short to be unique

What specific attack does salting each password defeat?

  • Precomputed rainbow tables
  • Brute-force guessing against a single stolen password hash
  • Timing attacks during password verification
  • Reversal of the hash back into the password

Why is argon2id preferred over PBKDF2 for new systems?

  • It is memory-hard, resisting GPU and ASIC cracking
  • PBKDF2 cannot use a salt, while argon2id can
  • argon2id is reversible, making password recovery possible
  • PBKDF2 stores the password in plaintext internally

You got correct