Password Hashing: Why SHA-256 Is the Wrong Choice
Published 2026-10-31
The same hash function can be right in one place and wrong in another:
- Verifying file integrity → must be fast. SHA-256 is correct.
- Storing user passwords → must be slow. SHA-256 is a liability.
The difference is who the attacker is and how they attack.
Fast hashes help the attacker
If a database leaks and it stored sha256(password), the attacker's job is to guess the input:
| Hash | Guesses per second on one high-end GPU | Time to exhaust an 8-character mixed-case password |
|---|---|---|
| SHA-256 | ~10^10 | days to weeks |
| bcrypt (cost 12) | ~10^4 | thousands of years |
SHA-256 was designed for throughput — verifying downloads, mining, signatures. GPUs parallelise it extremely well. bcrypt was designed for the opposite: make each evaluation expensive on purpose, so brute force does not scale.
So the problem is not that SHA-256 is weak. The problem is that it is fast, and speed is exactly what the attacker needs.
Salts solve a different problem
A salt defeats precomputation:
Without a salt, 123456 hashes to the same value everywhere. An attacker precomputes tens of millions of common passwords once and looks them up. Two users with the same password also produce the same hash — crack one, get both.
With a per-user random salt, identical passwords produce different hashes and every precomputed table becomes useless.
hash = SHA256(salt + password)
But note: a salt cannot make a fast hash safe. The salt is stored alongside the hash in plaintext, so the attacker simply recomputes per candidate. Salts raise the cost of bulk cracking, not the cost of one guess. To raise the per-guess cost you must make the function slow.
bcrypt: three practical details
Choosing the cost factor
The cost is an exponent: cost 12 means 2^12 = 4096 rounds. Each increment doubles the time.
| Cost | Approximate time | Verdict |
|---|---|---|
| 10 | 50–100 ms | Minimum acceptable |
| 12 | 200–400 ms | Recommended default |
| 14 | 1–2 s | High-security, login feels slow |
Aim for 200–500 ms per verification. Below that the attacker's advantage grows; above ~1 s users notice, and you have built a CPU exhaustion vector — login becomes a denial-of-service target. Rate-limit the endpoint.
The 72-byte limit
bcrypt only reads the first 72 bytes of the password and silently discards the rest.
Two long passphrases can therefore be equivalent. The standard workaround is to pre-hash: SHA-256 the password, Base64-encode the digest (44 characters), then feed that to bcrypt. Do both steps with UTF-8 and no truncation.
Always verify with the library's compare
const hash = await bcrypt.hash(password, 12);
// stored value looks like $2b$12$...
await bcrypt.compare(inputPassword, hash); // correct
Do not extract the salt and reimplement the pipeline. The cost factor and version ($2a$ vs $2b$ differ subtly) are encoded in the result string.
Also: never compare hashes with ===. String comparison returns early at the first differing byte, and that timing difference leaks information. Use a constant-time comparison (crypto.timingSafeEqual).
Argon2 is the current recommendation
bcrypt's weakness is that it is not memory-hard: it burns CPU but very little memory, which is exactly what GPUs and FPGAs are built for.
Argon2 (winner of the 2015 Password Hashing Competition) requires both time and memory, which removes the parallelism advantage. OWASP's current order of preference:
- Argon2id
- scrypt
- bcrypt (for legacy compatibility)
- PBKDF2 (the floor when compliance requires it)
New projects should start with Argon2id. Existing bcrypt hashes do not need an emergency migration — rehash on the next successful login and upgrade silently.
Generating and verifying
To see what a password looks like under cost 12, or to confirm that a stored $2b$12$... value matches a candidate, use bcrypt Generator — it runs locally in the browser.
For generating initial passwords, use Password Generator. Do not roll your own with Math.random(); it is not a cryptographically secure source.
The reverse case: file integrity needs a fast hash
When a vendor publishes a SHA-256 checksum for a download, you compute the digest and compare. This must be fast — a multi-gigabyte file should hash in seconds.
The decision rule is one question: does the data need to stay secret, and must the original be unrecoverable?
| Scenario | Use | Why |
|---|---|---|
| User passwords | Argon2id / bcrypt cost ≥ 12 | Must be slow, resistant to GPUs |
| File integrity | SHA-256 | Must be fast |
| Cache keys, deduplication | MD5 is fine | Collision risk is irrelevant here |
| Digital signatures | SHA-256 + RSA/ECC | The hash only compresses; the signature provides security |
| Base64 | None of the above | It is an encoding — see this |
The boundary to remember: hashing answers "is this the same thing", encryption answers "can someone else read it". A password hash is neither — it is a hash that is deliberately slow.