Start here: the problem it solves#
TL;DRthe 30-second version
- Never store the password. Store a one-way hash of it, so a leaked table doesn't hand over passwords.
- A plain fast hash is not enough. Attackers guess billions of passwords a second, and they reuse precomputed tables.
- Give every user a unique salt, so precomputed tables are useless. Make the hash deliberately slow, so each guess costs real time.
- On login, compare the two hashes in constant time, so how long a reject takes never leaks anything.
Say you run the users table for a service. Someone signs up with the password they use everywhere. You have to check that password on every login. But you must not keep it in a column, because that column will eventually be read by someone you didn't intend: a leaked backup, a wrong query, a copied replica.
The first move is to store a hash instead. A hash function turns any input into a fixed-size digest, and it can't be run backwards. So you store the digest. On login, you hash what the user typed and compare it to the digest. The password itself is never kept.
A plain hash is fast, and an attacker who steals the table doesn't need to reverse it. They guess. They hash a list of common passwords and look for matches. One GPU computes about ten billion SHA-256 hashes a second, so the ten million most common passwords are all tried in a fraction of a second. And if the hash has no salt, they hash that list once, keep it as a lookup table (a rainbow table), and reuse it against every leak forever.
The fix: salt it, slow it, compare it carefully#
Two things make guessing cheap: sharing work across users, and running fast. Close them one at a time.
First, sharing. A salt is a random value generated for each user and mixed into the password before hashing. Two users with the same password now get two different digests, so cracking one tells you nothing about the other. And a precomputed table is useless, because it was built without your users' salts. The salt is not a secret. It's stored in plain text right next to the hash. Its job is to be unique, not hidden.
Second, speed. Make the hash slow on purpose. Instead of hashing once, feed the result back through the hash thousands of times. The number of rounds is called the work factor, or cost. That is what a password-hashing function like bcrypt or Argon2 does. One login pays this once and doesn't notice a hundred milliseconds. An attacker trying billions of guesses pays it billions of times. And as hardware gets faster, you turn the dial up.
PredictThe salt is stored in plain text next to the hash. If an attacker steals the database, they have every salt. Why does salting still help?
Hint: Think about what the attacker can no longer reuse.
Because the salt's job is uniqueness, not secrecy. Knowing the salts doesn't let the attacker reverse anything. What it takes away is reuse: a precomputed table was built without these salts, so it's worthless, and cracking one account gives no head start on any other. The attacker has to run a fresh, full guessing attack against every account separately.
The last part is on the login path. You hash the attempt with the stored salt and cost, then compare it to the stored digest. If you compare character by character and stop at the first difference, the time it takes depends on how many leading characters matched. An attacker can measure that. A constant-time compare always checks every character, so a wrong guess takes the same time no matter where it went wrong.
What the work factor buys#
Take an 8-character password of lowercase letters and digits. That's 36 to the power 8, about 2.8 trillion possibilities. Against a plain SHA-256 at ten billion guesses a second, one GPU tries all of them in under five minutes. Each step up the work factor divides the attacker's rate, while your own login still takes a fraction of a second.
| Work factor (rounds per hash) | Attacker guesses/sec | Time to try every 8-char password |
|---|---|---|
| 1 (plain fast hash) | ~10 billion | ~5 minutes |
| 4,096 (bcrypt cost 12) | ~2.4 million | ~2 weeks |
| 100,000 | ~100 thousand | ~1 year |
| 600,000 (PBKDF2 floor) | ~17 thousand | ~5 years |
The exact figures move with hardware. The shape doesn't: the dial multiplies the attacker's time, not yours.
If this comes up in an interview#
Why not just hash the password with SHA-256?
SHA-256 is fast and has no salt, which are the two properties you don't want. Fast means an attacker guesses billions per second. No salt means one precomputed table cracks everyone. You want a slow, salted password-hashing function: Argon2, bcrypt, or scrypt.
What does the salt do, and can it be public?
It's a unique per-user value mixed in before hashing. It makes precomputed tables useless and stops identical passwords from sharing a digest. It is not secret. It's stored right next to the hash. Its job is uniqueness, not concealment.
What is the work factor and how do you pick it?
It's how many rounds the hash runs, which sets how expensive one guess is. Tune it so a single hash takes roughly 100 to 250 milliseconds on your login hardware, and raise it over time as hardware speeds up.
Why compare in constant time?
A naive comparison stops at the first differing character, so its timing reveals how many leading characters matched. A constant-time compare checks every character regardless, so a reject leaks nothing.
Does a slow hash make any password safe?
No. A password on a common wordlist is found within the length of that list no matter how slow each guess is. The slow hash only buys time against strong, unique passwords that force a search of the full keyspace.
Tuning it
- Every login runs the slow hash on your server, so a higher cost means more CPU per login. Tune to a target time (100 to 250 ms), not a fixed number, and revisit it as hardware improves.
- Because each attempt is expensive, the login endpoint becomes a denial-of-service target. Rate-limit attempts per account and per source address.
- A pepper is a secret value added to every hash and kept outside the database, in app config or a hardware module. If only the database leaks, the pepper isn't in it, so the stolen hashes are still uncrackable.
- Raising the cost later is easy. On the next successful login you briefly have the plaintext, so rehash at the new cost and replace the stored value. Store the algorithm, cost, and salt with the hash so old and new settings can coexist.
Which function to use
| Function | Makes the attacker spend | Notes |
|---|---|---|
| SHA-256 / MD5 (don't) | Nothing | General-purpose hashes are built to be fast. Right for checksums and signatures, wrong for passwords. |
| PBKDF2 | CPU (iterations) | Simple iterated hash, FIPS-approved. Cheap for GPUs, so it needs very high iteration counts (OWASP: ~600,000). |
| bcrypt | CPU + a little memory | Battle-tested since 1999. Cost is a base-2 exponent. Truncates input at 72 bytes. |
| scrypt | Memory + CPU | Memory-hard, so custom hardware gains less. Knobs for memory, block size, parallelism. |
| Argon2id | Memory + CPU | Winner of the Password Hashing Competition and the current default recommendation. |
Default to Argon2id. bcrypt is a fine, well-understood alternative. Use PBKDF2 only when a compliance rule requires a FIPS-approved primitive. Never invent your own scheme.
Mistakes, and the breaches that made them famous
- Using a fast hash (MD5, SHA-1, SHA-256) for passwords. LinkedIn, 2012: 6.5 million unsalted SHA-1 hashes leaked, and most were cracked within days using existing tables.
- Encrypting instead of hashing. Adobe, 2013: passwords were encrypted with a reversible cipher, so identical passwords produced identical ciphertext, and the plaintext password hints stored beside them cracked the set.
- No salt, or one salt shared by everyone. One precomputed table still cracks the whole database.
- Comparing hashes with an ordinary equality check. Use the library's constant-time compare.
References
- OWASP Password Storage Cheat Sheet — Current recommendations: Argon2id defaults, bcrypt and PBKDF2 parameters, peppering.
- NIST SP 800-63B, Digital Identity Guidelines — Salting, key stretching, and why length beats forced complexity.
- RFC 9106, Argon2 — The specification and parameter guidance for Argon2id.
- Provos & Mazières, A Future-Adaptable Password Scheme (bcrypt) — The 1999 paper that introduced a tunable cost factor.