Why Plain SHA-256 Is Wrong For Passwords
Suppose a database leaks. It contains 100 million rows, each storing SHA-256(password). How bad is this?
An attacker downloads the dump and starts cracking. A single consumer GPU computes about 10 billion SHA-256 hashes per second. Attacking an 8-character password using the typical character set (94 printable ASCII chars) requires at most 94^8 = 6 quadrillion attempts. Divided by 10 billion hashes/sec:
- One GPU brute-forces the worst 8-character password space in about 1 week.
- A small rig of 8 GPUs does it in about 21 hours.
- Most passwords are nowhere near random and fall to dictionary attacks in seconds.
The attacker is not trying random strings against a single password. They are pre-computing hashes of a dictionary of common passwords (and their variations) and comparing against every row in the dump in parallel. A single dictionary run against the entire 100M-row leak completes in seconds. SHA-256 is the wrong tool for the job: its speed is what gives the attacker the advantage.
LinkedIn stored 117 million user passwords as unsalted SHA-1 hashes. When the dump leaked, security researchers cracked 90% of the passwords within days using consumer hardware. Many users had reused those passwords on other sites. The cascade of secondary breaches lasted years. The root cause was using a general-purpose hash for password storage, which is fast by design.
The Three Things Password Hashes Need
Password hashes are not general-purpose hashes. They have specific design goals:
- Slow on purpose. A single hash computation should take 100 milliseconds or more, not microseconds. A legitimate user logs in once and waits a tenth of a second. An attacker trying to brute-force ten billion candidates suddenly faces ten billion tenths of a second.
- Memory-hard. The algorithm should require a significant amount of RAM to compute. This penalizes GPU and ASIC attackers, who have lots of compute cores but limited per-core memory. Memory-hard means the speed advantage of specialized hardware shrinks.
- Configurable. Hardware gets faster every year. The algorithm should expose tuning parameters (iteration counts, memory size, parallelism) so defenders can keep pace. What was slow in 2015 is fast in 2026.
The Workhorses: bcrypt, scrypt, Argon2
| Algorithm | Year | Properties | Where it lives |
|---|---|---|---|
| bcrypt | 1999 | CPU-slow, fixed small memory. Cost parameter (work factor). | Ruby on Rails, Django defaults until 2019, many legacy systems. Still acceptable. |
| PBKDF2 | 2000 (RFC 2898) | Just SHA repeated N times. Not memory-hard. Configurable iteration count. | WPA2/WPA3, LUKS disk encryption, browser Web Crypto API. Acceptable when nothing better is available, but not preferred. |
| scrypt | 2009 | Memory-hard. Three parameters: N (cost), r (block size), p (parallelism). | Litecoin, Tarsnap, some password managers. Largely superseded by Argon2. |
| Argon2 (id variant) | 2015 | Memory-hard, parallel-resistant. Winner of the Password Hashing Competition. | OWASP recommended default. Used by Bitwarden, 1Password, modern Linux logins (libpam-argon2), Signal, Matrix. |
OWASP recommendation, current as of 2026: use Argon2id with minimum parameters m=19MiB, t=2, p=1. Fall back to scrypt or bcrypt if Argon2 is unavailable on your platform. Never use plain SHA-256, MD5, or single-round HMAC for passwords.
Work Factors and Future-Proofing
Every password hash function takes a tuning parameter that controls how slow it runs. bcrypt calls it the "cost", PBKDF2 the "iteration count", Argon2 has three parameters (m, t, p).
The pattern for defenders: pick parameters that take about 100 to 250 milliseconds on your server's CPU. Re-evaluate annually. When the next CPU generation lands and your hash falls to 60 ms, raise the cost.
Modern stored password hashes encode the parameters inline so you can verify old passwords with their old cost and silently re-hash on the next successful login:
$argon2id$v=19$m=19456,t=2,p=1$IEvxhuekN+8B5RxRMz6XnQ$cMtq8VqcFiRrLeRfb+5gZQ ^ ^ ^ ^ ^ | | | | | | | parameters salt (base64) hash (base64) | | algo version
That single string is everything you need to store. The salt is in there. The parameters are in there. The algorithm is in there. No separate columns required.
Why Slow Matters: A Live Timing Comparison
The interactive runs the same password through plain SHA-256 (one round) and PBKDF2-SHA-256 (configurable rounds) and times both. Then it computes how many guesses per second an attacker with a $1,000 GPU could do against each.
Plain hash vs PBKDF2: timing and attacker throughput
Type a password. Set the PBKDF2 iteration count with the slider. Click Hash. Watch how SHA-256 finishes in microseconds while PBKDF2 takes hundreds of milliseconds. The attacker-throughput numbers translate that time into "guesses per second a GPU adversary could do" against a leaked database storing this format.
Read the difference: PBKDF2 is roughly 100,000x to 1,000,000x slower than plain SHA-256 at the recommended iteration count. That same factor applies to every attacker guess. A dictionary attack that finishes in seconds against plain SHA-256 takes weeks or months against properly tuned PBKDF2. With Argon2id (memory-hard, not browser-available), the slowdown for GPU attackers is even larger because they cannot parallelize as efficiently.
Common Mistakes
- Storing passwords in plaintext. Happens more than it should. If your database backup leaks, every password is exposed instantly.
- Hashing with MD5 or single-round SHA. Modern GPUs crack hundreds of millions of dictionary entries per second. Same outcome as plaintext for most user passwords.
- No salt. Without a per-user salt, identical passwords across users hash identically, enabling precomputed rainbow table attacks. The Salting page covers this.
- Salt stored in a separate, leaky column. Salts don't need to be secret, but if you store them in a config file the dump doesn't include, you've split your secret. Stick salts inline with the hash.
- Hashing client-side and storing the result. The hash IS the credential, so this just moves the password to a different bytes-on-the-wire. Hash on the server, every time.
- Not migrating old hashes. Increase your work factor over time. Re-hash on successful login. Stored bcrypt hashes from 2010 with cost=4 are weak in 2026.
- Using
SHA-256"twice" or 1000 times as a substitute for PBKDF2. Roll-your-own slow hashing always loses to a real KDF. Use Argon2id, scrypt, bcrypt, or PBKDF2.