Password Storage Done Right
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.
# 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.
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.
- 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.
- 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).
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