Why Websites Shouldn't Know Your Password
When you log in, a well-built site doesn't store your password. It stores a scrambled fingerprint of it, called a hash. At login, it hashes what you typed and compares fingerprints.
The reason is simple: databases leak. When they do, attackers shouldn't be able to read everyone's passwords. Whether they can depends almost entirely on how the passwords were hashed, and that comes down to some simple math.
🎯 Quick Answer (30-Second Read)
- Hashing is one-way: easy to compute, practically impossible to reverse. Encryption is two-way, so it's the wrong tool for passwords.
- Attackers don't reverse hashes, they guess: hash billions of candidate passwords and look for matches.
- Salts (random values per user) stop attackers from cracking everyone at once.
- Fast hashes (MD5, SHA-256) are bad for passwords because speed helps the attacker.
- Use a slow, memory-hard hash: OWASP recommends Argon2id (min 19 MiB memory, 2 iterations, 1 thread), with scrypt, bcrypt (cost 10+) or PBKDF2 (600,000+ iterations) as alternatives.
Hashing vs Encryption
Data + key → ciphertext → back to data with the key. If the key leaks, every password leaks.
Password → fixed-length fingerprint. There's no key and no way back. You can only check a guess.
How Login Works With Hashing
The Attacker's Math
After a breach, an attacker has hashes, not passwords. Their only move is guessing: hash a candidate, compare, repeat. So the question is how many guesses per second they can make, and how many guesses they need.
Number of possible passwords = (characters available) ^ (length)
| Password type | Possibilities |
|---|---|
| 8 lowercase letters | 26⁸ ≈ 209 billion |
| 8 chars, upper + lower + digits | 62⁸ ≈ 218 trillion |
| 12 chars, upper + lower + digits | 62¹² ≈ 3.2 × 10²¹ |
Time to try every option = possibilities ÷ guesses per second.
Fast hashes like MD5 and SHA-256 were designed for speed: modern GPUs compute billions of them per second. At 10 billion guesses per second, all 209 billion 8-letter lowercase passwords take about 21 seconds.
A deliberately slow password hash changes the denominator. If each guess costs a few thousand times more work, the same search takes hours or days instead of seconds, and the attacker's electricity bill explodes.
Time to try all 209 billion 8-letter lowercase passwords, for illustration. Slowing each guess 1,000,000× turns seconds into months.
In reality, attackers try likely passwords first (leaked password lists, common words, "Password@123"), so weak passwords fall fast no matter what. Slow hashing protects the reasonable passwords.
Salts: Stopping Mass Cracking
Without salts, everyone with the password iloveyou has the same hash. Attackers precompute hashes of common passwords once (in "rainbow tables") and crack every matching account instantly.
A salt is a random value generated for each user and mixed in before hashing:
| User | Password | Salt | Hash |
|---|---|---|---|
| Asha | iloveyou | 9f2c… |
a71e… |
| Ravi | iloveyou | 41bd… |
c03f… |
Same password, different hashes. Precomputed tables become useless, and the attacker must crack each user separately. Salts aren't secret. They're stored next to the hash.
Why Memory-Hard Hashing Matters
GPUs and custom chips are very good at running many simple calculations in parallel. Argon2id and scrypt make each guess need a chunk of memory (OWASP's minimum for Argon2id is 19 MiB per hash). Memory is expensive to multiply across thousands of parallel guesses, so attackers lose much of their hardware advantage.
OWASP's Recommended Settings
| Algorithm | Minimum setting | When to use |
|---|---|---|
| Argon2id | 19 MiB memory, 2 iterations, parallelism 1 | First choice |
| scrypt | N = 2¹⁷, r = 8, p = 1 | If Argon2id isn't available |
| bcrypt | Work factor 10+ (72-byte password limit) | Legacy systems |
| PBKDF2 | 600,000+ iterations, HMAC-SHA-256 | When FIPS-140 compliance is required |
In Python, for example:
from argon2 import PasswordHasher # pip install argon2-cffi
ph = PasswordHasher() # library defaults exceed OWASP's minimum
stored = ph.hash("mango42") # salt is generated and embedded automatically
ph.verify(stored, "mango42") # returns True, raises an error if wrongChecklist for Developers
- Never store plain-text or encrypted passwords
- Never use MD5, SHA-1 or plain SHA-256 for passwords
- Use Argon2id (or the alternatives above) via a well-known library
- Let the library handle salts
- Rate-limit login attempts and support two-factor authentication
- Re-hash with stronger settings when users next log in
Frequently Asked Questions
Why is SHA-256 bad for passwords?
SHA-256 is secure as a general hash, but it's designed to be fast. Speed lets attackers test billions of guesses per second with GPUs. Password hashes like Argon2id, scrypt and bcrypt are deliberately slow and tunable, which makes guessing far more expensive.
Is bcrypt still safe?
bcrypt with a work factor of 10 or more is still acceptable, and OWASP lists it for legacy systems. It has a 72-byte password limit and isn't memory-hard, so new projects should prefer Argon2id where available.
What is a salt in password hashing?
A salt is a random value generated for each password and combined with it before hashing. It makes identical passwords produce different hashes, which defeats precomputed rainbow tables and forces attackers to crack each account separately. Salts are stored alongside the hash.
Conclusion
Password security is a numbers game. Attackers can't reverse hashes, so they guess, and the cost of each guess decides how long your users stay safe. Salts stop mass cracking, slow memory-hard hashes like Argon2id make each guess expensive, and good libraries handle both for you.
Sources
Last verified: 14 October 2026.
Related reads: Fix Supabase RLS Policy Error · Rate Limiter Explained · How Does Face ID Work?
I explain the tech behind everyday products. Follow on LinkedIn or X, or invite me to speak.