MD5, and the speed that ruins it.
MD5 turns any input into 128 bits, and it does so fast enough that a laptop manages millions of them per second. For checking a downloaded file that speed is the feature. For storing a password it is the whole problem.
Everything here runs locally
The hash updates as you type. There is no button to press.
How fast is too fast?
This measures your own browser rather than quoting a benchmark. It hashes two hundred thousand different strings and reports the rate. A graphics card built for the job runs several orders of magnitude beyond whatever number you get here.
Where MD5 is still fine
It is worth being precise about this, because the blanket advice to never use MD5 is not quite right. The failures are specific.
- File integrity
- Fine. If you are checking that a download was not corrupted in transit, MD5 does the job and does it quickly. It stops being enough the moment you need to prove the file was not swapped deliberately.
- Deduplication and cache keys
- Fine. Nobody is constructing collisions to poison your cache of thumbnail images, and the speed is welcome.
- Digital signatures and certificates
- Broken. Collisions have been practical since 2004, and a real certificate forgery was demonstrated in 2008. Use SHA-256.
- Password storage
- Broken, for a different reason. Not because of collisions but because it is fast, unsalted by default, and every common password already sits in a lookup table somewhere.
If you are stuck with MD5 password hashes
You cannot recover the passwords, and you should not wait for everyone to log in before those rows get better protection. Hash the existing MD5 values with bcrypt and store the result. Every account is covered as soon as the batch job finishes.
// one pass over the table, no user involvement
user.passwordHash = await bcrypt.hash(user.md5Hash, 12);
user.scheme = 'bcrypt-over-md5';
// then at login
if (user.scheme === 'bcrypt-over-md5') {
const ok = await bcrypt.compare(md5(candidate), user.passwordHash);
if (ok) {
user.passwordHash = await bcrypt.hash(candidate, 12);
user.scheme = 'bcrypt';
}
}The migration write up goes through the same pattern in more detail, including what to widen before you start.