How to Migrate MD5 Password Hashes to bcrypt
Migrating from MD5 to bcrypt doesn't require a forced password-reset email. Detect the hash type at login time, verify with the old algorithm, rehash with bcrypt on success, and update the stored value - every user upgrades silently the first time they sign in.
Why MD5 is not safe for passwords
MD5 was designed for speed. A modern GPU can test billions of MD5 hashes per second, which means a leaked password table is cracked in hours, not centuries. bcrypt is designed to be slow - a cost factor of 10 takes roughly 100ms per hash on a server, which is imperceptible to a real user but makes a brute-force attack millions of times more expensive.
The two changes you need
1. New passwords use bcrypt:
import bcrypt
def hash_password(password: str) -> str:
return bcrypt.hashpw(password.encode(), bcrypt.gensalt(rounds=10)).decode()
bcrypt.gensalt(rounds=10) generates a per-user salt and embeds it in the
resulting $2b$10$... string, so you store one value and need no separate salt
column.
2. Login detects MD5 vs bcrypt and rehashes on upgrade:
import hashlib
def _is_md5(h: str) -> bool:
return len(h) == 32 and all(c in "0123456789abcdef" for c in h)
def verify_password(stored_hash: str, password: str) -> bool:
if _is_md5(stored_hash):
return hashlib.md5(password.encode()).hexdigest() == stored_hash
return bcrypt.checkpw(password.encode(), stored_hash.encode())
Then in the login path, after a successful verify, silently upgrade any legacy MD5 hash:
if not verify_password(stored_hash, password):
return False
if _is_md5(stored_hash):
new_hash = hash_password(password)
db.execute(
"UPDATE users SET password_hash = %s WHERE id = %s",
(new_hash, user_id)
)
return True
Why this works without a flag day
The stored hash itself tells you which algorithm produced it - a 32-character
hex string is MD5; a string starting with $2b$ is bcrypt. No extra database
column is required. After one login cycle per user, every row in the table is
bcrypt. New signups go straight to bcrypt from day one.
Choosing a cost factor
Cost factor 10 is a reasonable default: it targets roughly 100-250ms on current hardware. Tune upward as hardware improves - the point is that the cost grows with attacker hardware too. If you're starting fresh today, argon2id (winner of the Password Hashing Competition) is the current recommendation, but bcrypt with a proper cost factor is still widely accepted.
Want to try it hands-on? HeyDevJob gives you this exact setup in a live cloud workspace in your browser - edit it, run it, and see it work. Free, nothing to install.
Try it in a workspace →What you'll practice
- Detecting MD5 vs bcrypt hashes at login to pick the right verify path
- Silently rehashing legacy MD5 passwords with bcrypt on first successful login
- Replacing hash_password with bcrypt.hashpw + bcrypt.gensalt for new signups
FAQ
How do I migrate MD5 password hashes to bcrypt without resetting every user's password?
Use a silent-rehash-on-login strategy: when a user logs in, detect whether their stored hash is MD5 (32 hex chars) or bcrypt ($2b$ prefix), verify with the matching algorithm, and if they had an MD5 hash, rehash their plaintext password with bcrypt and update the stored value. No forced reset needed.
How does bcrypt compare to MD5 for password hashing?
MD5 is a fast general-purpose hash - a GPU can test billions per second, making brute-force attacks fast. bcrypt has a tunable cost factor that intentionally slows hashing to ~100-250ms per attempt, making brute-force millions of times more expensive even with a leaked database.
What cost factor should I use for bcrypt?
Start at rounds=10 (bcrypt.gensalt(rounds=10)), which targets roughly 100-250ms on current hardware. Calibrate so a legitimate login feels instant but is meaningfully slow for an attacker. Bump the factor as hardware improves - bcrypt's design lets you re-hash existing hashes at a higher cost on next login.
Keep learning
Learn it by doing. Open this in a live cloud workspace, make the change yourself, and keep a record of the work you can share.
Open the workspace →