You inherited a database full of MD5 or SHA-1 password hashes. You know these algorithms are broken. You know that if your database is compromised, every password will be cracked in minutes. You want to migrate to bcrypt. But you cannot just reset everyone's password, because half your users would never come back.
This is one of the most common problems in application security, and it has a well-known solution. Let's walk through the migration pattern that production applications use to upgrade password hashing without downtime, without resets, and without losing users.
Why MD5 and SHA-1 Are Dangerous for Passwords
Before we get to the migration, let's be clear about why you need to do it.
MD5 and SHA-1 are general-purpose hash functions designed to be fast. That is their job. They are meant to verify file integrity, build hash tables, and support cryptographic protocols. They are not designed to slow down attackers.
On a modern GPU, an attacker can compute: - **MD5**: roughly 25 billion hashes per second - **SHA-1**: roughly 8 billion hashes per second - **Bcrypt (cost 12)**: roughly 2 to 3 hashes per second
If your database is stolen and your passwords are hashed with MD5, an attacker can try the top 10,000 most common passwords against every single user in your database in seconds. For a database with 1 million users, that is 10 billion hashes, which takes less than a second at 25 billion hashes per second.
With bcrypt at cost 12, the same attack takes over 100 years. The difference is not a matter of degree. It is the difference between "every weak password is cracked instantly" and "the attack is not worth attempting."
If you are still using MD5 or SHA-1, this is not a theoretical risk. It is a ticking time bomb. You need to migrate.
The Dual-Hash Migration Strategy
The key insight is that you do not need to know a user's plaintext password to re-hash it with bcrypt. You just need to know that the password they typed is correct. And you can verify that using the old hash.
Here is the strategy, step by step.
### Step 1: Wrap the Old Hashes in Bcrypt
For each user, take their existing MD5 or SHA-1 hash and treat it as the input to bcrypt. In other words, you bcrypt-hash the old hash.
old_hash = MD5(user_password) // already stored in database new_hash = bcrypt(old_hash, cost=12) // wrap it in bcrypt
Store the new bcrypt hash in place of the old one. You can add a column to track which users have been fully migrated and which are still using wrapped hashes.
At this point, every user in your database has a bcrypt-protected hash. Even if the database is stolen right now, an attacker has to crack the bcrypt layer before they can even start on the MD5 layer. You have immediately improved your security posture.
### Step 2: Verify Passwords Using the Wrapped Hash
When a user logs in, here is how you verify their password:
1. Take the submitted plaintext password. 2. Compute the old hash: old_hash = MD5(submitted_password). 3. Verify the old hash against the stored bcrypt hash: bcrypt.verify(old_hash, stored_bcrypt_hash). 4. If it matches, the password is correct.
This works because bcrypt is just checking whether the MD5 of the submitted password matches the MD5 that was wrapped in bcrypt. If the user typed the right password, the MD5 will match, and bcrypt will confirm it.
### Step 3: Re-Hash with Bcrypt on Successful Login
Here is where the actual migration happens. When a user successfully logs in using the wrapped hash, you now have their plaintext password (they just typed it). You can hash it directly with bcrypt, replacing the wrapped hash.
if bcrypt.verify(MD5(submitted_password), stored_hash): // Password is correct. User is authenticated. // Now upgrade: hash the plaintext password directly with bcrypt. new_hash = bcrypt(submitted_password, cost=12) store new_hash in database mark user as fully migrated
From now on, this user's password is stored as a direct bcrypt hash. No more MD5 wrapping. On future logins, you just verify the submitted password directly against the bcrypt hash.
### Step 4: Handle Already-Migrated Users
For users who have already been migrated, the login flow is simple: just verify the submitted password directly against the stored bcrypt hash. You can check the migration flag to determine which path to take.
Most bcrypt libraries make this easy. The verify function will work regardless of the cost factor or salt in the stored hash, so you do not need to worry about which cost factor a particular user's hash uses.
What About Users Who Never Log In?
Some users will not log in for months or even years. Their passwords will remain as wrapped MD5-in-bcrypt hashes. Is that a problem?
Not really. A wrapped hash is still much more secure than a plain MD5 hash. An attacker who steals the database would need to crack the bcrypt layer first (which is slow), and then crack the MD5 layer underneath (which is fast, but they can only do it one at a time after breaking each bcrypt hash). The bcrypt layer effectively rate-limits the entire cracking process.
The wrapped hash is not as good as a direct bcrypt hash, but it is a massive improvement over plain MD5. And it gets better over time as more users log in and get migrated.
After a reasonable period (say 6 to 12 months), you can evaluate how many users are still on wrapped hashes. For those who have not logged in, you can either leave them as-is (still more secure than before), send a password reset email, or eventually expire their passwords and require a reset on next login.
Testing the Migration
Before deploying this to production, test thoroughly. Here is a checklist:
**Unit tests:** Verify that a password hashed with MD5, then wrapped in bcrypt, can be verified correctly. Verify that after migration, the direct bcrypt hash verifies correctly.
**Edge cases:** Test empty passwords, passwords with special characters, passwords longer than 72 bytes (bcrypt truncates, so the MD5 of the full password will not match the bcrypt of the truncated password, see our 72-byte limit post for details on handling this).
**Rollback plan:** Make sure you can roll back if something goes wrong. Keep the old hashes until you are confident the migration is working. Do not overwrite them until you have verified that the new verification flow works.
**Monitoring:** Track how many users have been migrated over time. This will tell you how quickly the migration is progressing and whether you need to take additional action for users who have not logged in.
A Note on SHA-256
If you are using plain SHA-256 (without a salt or with a static salt), the same migration strategy applies. SHA-256 is not broken like MD5 or SHA-1, but it is still far too fast for password hashing. The dual-hash migration works exactly the same way: wrap the SHA-256 hash in bcrypt, then gradually replace with direct bcrypt hashes as users log in.
For a deeper understanding of why bcrypt is the right target for this migration, read our bcrypt explained post.
The Bottom Line
Migrating from MD5 or SHA-1 to bcrypt does not require a big-bang rewrite or a mass password reset. The dual-hash strategy lets you improve security immediately (by wrapping old hashes in bcrypt) and complete the migration gradually (by re-hashing passwords directly as users log in).
The most important thing is to start. Every day your database contains plain MD5 or SHA-1 hashes is a day you are one breach away from having every password cracked. Wrap those hashes in bcrypt today, and the rest of the migration will take care of itself over time.
Ready to generate bcrypt hashes for the migration? Try our bcrypt generator to test different cost factors and see the output format.