bcrypt vs MD5 for Password Storage
Core Problem: Why MD5 Can't Store Passwords
MD5's fundamental problem for password storage is speed. MD5 can compute billions of hashes per second โ an advantage for data processing but a fatal flaw for password security. When a database is breached, an attacker with MD5 hashes can crack them using: rainbow tables (pre-computed hashโpassword lookup tables), brute force (billions of password combinations per second on modern GPUs), dictionary attacks (using common password wordlists). Most weak passwords' MD5 hashes can be cracked in seconds or even milliseconds.
bcrypt's Design Philosophy: Deliberately Slow
bcrypt was designed by Niels Provos and David Maziรจres in 1999, with the core philosophy of being "deliberately slow" โ controlling computational complexity through the cost parameter. Cost=10 means 2^10 (1,024) rounds of key schedule iterations. On modern computers, bcrypt(cost=12) takes about 100โ300 milliseconds, while MD5 takes microseconds. This difference increases brute-force costs by orders of magnitude: MD5 GPU cracking ~10 billion/sec, bcrypt cost=12 GPU cracking ~100,000/sec โ about 100,000x difference.
bcrypt's Built-in Security Features
- Automatic salt generation: bcrypt automatically generates a random salt on every hash and embeds it in the output โ no manual salt management needed, preventing rainbow table attacks
- Adjustable cost factor: As hardware improves, increase the cost value to maintain security without rewriting code
- Output contains all verification information: bcrypt output string includes algorithm version, cost factor, salt, and hash โ verification only needs the original password and this string
/* bcrypt output format */
$2b$12$[22-char salt][31-char hash]
^ ^ ^
| | |-- 128-bit salt (22 Base64 chars)
| |-- cost factor (2^12 = 4096 iterations)
|-- bcrypt version (2b = current standard)
Using bcrypt in Common Frameworks
// Node.js
const bcrypt = require('bcryptjs');
// Hash (async)
const hash = await bcrypt.hash(password, 12); // cost=12
// Store hash in database
// Verify
const isValid = await bcrypt.compare(password, storedHash);
// Python (Django)
from django.contrib.auth.hashers import make_password, check_password
hash = make_password(password) # Uses PBKDF2 by default, can configure bcrypt
is_valid = check_password(password, hash)
# PHP
$hash = password_hash($password, PASSWORD_BCRYPT, ['cost' => 12]);
$isValid = password_verify($password, $hash);
# Go (golang.org/x/crypto/bcrypt)
hash, _ := bcrypt.GenerateFromPassword([]byte(password), 12)
err := bcrypt.CompareHashAndPassword(hash, []byte(password))
bcrypt's Limitations
bcrypt has a practical limitation: password length is capped at 72 bytes. Characters beyond 72 bytes (about 72 ASCII characters) don't affect the hash result. For systems supporting very long passwords, you can SHA256-hash the password first (compressing any length to 32 bytes), then use the SHA256 hash (as hex string or Base64) as bcrypt input.
2025 Recommendation: Prefer Argon2id
Although bcrypt is still considered secure, new projects in 2025 should prefer Argon2id โ the winner of the 2015 Password Hashing Competition. Compared to bcrypt, Argon2id adds a memory complexity parameter, making it more resistant to GPU parallel attacks, with no 72-byte length limit. OWASP lists Argon2id as the first choice in its password storage guidelines, with bcrypt as the alternative.
Migration Guide: From MD5 Passwords to bcrypt
If your system uses MD5 for password storage, here's a strategy for migrating to bcrypt: When a user successfully logs in (you have the plaintext password), replace the MD5 hash with a bcrypt hash; for long-inactive users, bcrypt the MD5 hash value itself first ("hash of hash"), then complete the full migration on their next login; add an identifier field in the database to record each user's password hash type.
Try the free tool now
Use Free Tool โ