The Checklist First
Before the explanation, here's what you must do to store passwords correctly. Every item matters.
- Never store plaintext passwords
- Never use MD5, SHA-1, or SHA-256 alone for passwords
- Use bcrypt, Argon2id, or scrypt — not a general-purpose hash
- Let the library generate the salt automatically — don't do it manually
- Set a work factor that makes hashing take 100–300ms on your server
- Enforce a minimum password length of 8 characters, maximum of 72 characters (bcrypt limit)
- Hash the password on the server, not the client
- Never log passwords, even in error messages
- Implement rate limiting and account lockout on login endpoints
Why MD5 and SHA-256 Are Wrong for Passwords
This is the most misunderstood point in password security. MD5, SHA-1, and SHA-256 are fast. Extremely fast. A modern GPU can compute 10 billion MD5 hashes per second. That means a cracker can test every 8-character lowercase password in about 30 seconds against an MD5-hashed database.
Password hashing algorithms like bcrypt, Argon2, and scrypt are deliberately slow. They're designed to be tunable so you can make hashing take exactly as long as your server can tolerate (typically 100–300ms). That same 30-second crack becomes 3,000 years.
The other problem with raw SHA-256: no salt. If two users have the same password, their hashes are identical. An attacker can crack thousands of accounts at once by finding one matching hash in a rainbow table.
The Right Algorithms
bcrypt
The most widely supported option. Available in every major language. Salt is built in — bcrypt generates and stores the salt inside the hash string automatically.
Work factor (rounds) is a power of 2: rounds=12 means 2¹² = 4,096 iterations. Increase it as your hardware gets faster. The industry standard starting point is 10–12.
// Node.js with bcryptjs
const bcrypt = require('bcryptjs');
// Hash on registration
const hash = await bcrypt.hash(plainPassword, 12); // 12 rounds
// Store 'hash' in your database
// Verify on login
const isMatch = await bcrypt.compare(plainPassword, storedHash);
// Returns true/false — timing-safe comparison built in
# Python with bcrypt
import bcrypt
# Hash on registration
salt = bcrypt.gensalt(rounds=12)
hashed = bcrypt.hashpw(password.encode(), salt)
# Store 'hashed' (bytes) in database
# Verify on login
is_match = bcrypt.checkpw(password.encode(), stored_hash)
Bcrypt limitations: Maximum input is 72 bytes. Passwords longer than 72 characters are silently truncated. If you want to support longer passwords, pre-hash with SHA-256 and base64-encode before passing to bcrypt:
const crypto = require('crypto');
const prehash = crypto.createHash('sha256').update(password).digest('base64');
const hash = await bcrypt.hash(prehash, 12);
Argon2id
Winner of the 2015 Password Hashing Competition. More configurable than bcrypt: you tune memory usage (RAM), parallelism (CPU threads), and iterations separately. Argon2id is the hybrid variant — resistant to both GPU and side-channel attacks.
// Node.js with argon2
const argon2 = require('argon2');
const hash = await argon2.hash(password, {
type: argon2.argon2id,
memoryCost: 65536, // 64 MB
timeCost: 3, // 3 iterations
parallelism: 4, // 4 threads
});
const isMatch = await argon2.verify(hash, password);
# Python with argon2-cffi
from argon2 import PasswordHasher
ph = PasswordHasher(time_cost=3, memory_cost=65536, parallelism=4)
hash = ph.hash(password)
try:
ph.verify(hash, password)
# login success
if ph.check_needs_rehash(hash):
new_hash = ph.hash(password)
# update database with new_hash
except Exception:
pass # login failure
scrypt
Built into Python's standard library as of 3.6 and Node.js's crypto module. Good choice if you can't add external dependencies.
// Node.js built-in crypto
const crypto = require('crypto');
const util = require('util');
const scrypt = util.promisify(crypto.scrypt);
const salt = crypto.randomBytes(32);
const hash = await scrypt(password, salt, 64);
// Store: salt.toString('hex') + ':' + hash.toString('hex')
Algorithm Comparison
| Algorithm | Salt built-in | GPU resistance | Memory hardness | Recommended? |
|---|---|---|---|---|
| MD5 | No | Very weak | No | ❌ Never |
| SHA-256 | No | Very weak | No | ❌ Never |
| bcrypt | Yes | Moderate | No | ✅ Yes |
| scrypt | Manual | Strong | Yes | ✅ Yes |
| Argon2id | Yes | Strong | Yes | ✅ Best choice |
| PBKDF2 | Manual | Moderate | No | ✅ If others unavailable |
Setting the Right Work Factor
The goal: hashing one password should take 100–300ms on your production server. Too fast = easy to crack. Too slow = DoS risk from attackers spamming your login endpoint.
// Benchmark script — run this on your production hardware
const bcrypt = require('bcryptjs');
for (let rounds = 10; rounds <= 14; rounds++) {
const start = Date.now();
await bcrypt.hash('benchmark', rounds);
console.log(`rounds=${rounds}: ${Date.now() - start}ms`);
}
// Pick the highest rounds value under 300ms
Typical results on a modern server:
- rounds=10: ~65ms
- rounds=12: ~250ms ← good default
- rounds=14: ~1000ms ← too slow for most apps
What Your Database Should Store
For bcrypt, the entire hash string includes the algorithm, version, rounds, salt, and hash — all in one string like:
$2b$12$LrhasAakElf4YCRZzEJxXONMpBRrAVbKCMSJIJSDJSJDHEIJJ
Store this entire string in a single VARCHAR(255) or TEXT column. Never store the salt separately. Never store any metadata about the algorithm in a separate column — it's all in the hash string already.
Checking Existing Password Storage
If you're auditing an existing application:
-- PostgreSQL: look for unhashed passwords (plain text under 50 chars, no hash prefix)
SELECT COUNT(*) FROM users WHERE LENGTH(password_hash) < 20;
-- Look for MD5 hashes (32 hex chars)
SELECT COUNT(*) FROM users WHERE password_hash ~ '^[a-f0-9]{32}#39;;
-- Look for SHA-256 hashes (64 hex chars)
SELECT COUNT(*) FROM users WHERE password_hash ~ '^[a-f0-9]{64}#39;;
-- Look for bcrypt hashes (correct)
SELECT COUNT(*) FROM users WHERE password_hash LIKE '$2%';
→ Test bcrypt hash generation and verification in your browser with the Bcrypt Tool — useful for verifying what your library produces.