Introduction
Password hashing is critical for security in all modern applications. Bcrypt is the industry standard for securely hashing passwords and has been for over 20 years. In this guide, youβll learn why bcrypt is so important and how to implement it correctly.
Why Not Just Store Passwords as Plain Text?
π¨ What Happens in a Data Breach?
If passwords are stored in plain text:
- Hackers get direct access to all accounts
- Users who reuse passwords are threatened on other platforms
- GDPR violations and large fines
- Total loss of user trust
Examples from history:
- LinkedIn 2012: 6.5 million passwords stolen in plain text
- Adobe 2013: 153 million user accounts compromised
- Yahoo 2013-2014: 3 billion accounts affected
β The Solution: Hashing
Hashing converts passwords to a fixed-length string that cannot be converted back:
hash("password123") = "5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8"
But common hash functions (MD5, SHA-256) are still unsafe for passwords!
Why Not MD5 or SHA-256?
Problem with Fast Hash Functions
MD5/SHA-256 are designed for speed:
- MD5: ~500 million hashes/second
- SHA-256: ~200 million hashes/second
Why this is dangerous:
Common password: "password123"
SHA-256: "ef92b778bafe771e89245b89ecbc08a44a4e166c06659911881f383d4473e94f"
With GPU: 200 million hashes/second
Time to test 100 million common passwords: < 1 second! β‘
Hackers can easily brute-force passwords with rainbow tables and GPU clusters.
What is bcrypt?
Bcrypt is an adaptive hash function specifically designed for passwords:
Key Features
- Adaptive: Cost factor can be increased as hardware gets faster
- Intentionally slow: ~10-100 hashes/second (thousands of times slower)
- Built-in salt: Automatic salt generation
- Proven track record: Used since 1999
How bcrypt Works
bcrypt(password, cost_factor) = "$2a$10$N9qo8uLOickgx2ZMRZoMye..."
β β β
β β ββ Salt (22 chars)
β βββββ Cost factor (10 = 2^10 rounds)
ββββββββ Algorithm version ($2a$)
Cost factor 10 = 2^10 = 1,024 iterations Cost factor 12 = 2^12 = 4,096 iterations
Bcrypt vs Alternatives
| Method | Speed | Recommended? | Use Case |
|---|---|---|---|
| MD5 | 500M/sec | β Never | Legacy systems |
| SHA-256 | 200M/sec | β Never | File verification, not passwords |
| bcrypt | 10-100/sec | β Yes | Passwords (standard) |
| scrypt | Varies | β Yes | Passwords (more modern) |
| Argon2 | Varies | β Yes | Passwords (most secure) |
| PBKDF2 | Varies | β οΈ OK | Legacy compatibility |
Implementation
Node.js/JavaScript
Use bcryptjs or bcrypt:
const bcrypt = require("bcryptjs");
// Hash password
const saltRounds = 10;
const hashedPassword = await bcrypt.hash("myPassword", saltRounds);
// Result: $2a$10$N9qo8uLOickgx2ZMRZoMye...
// Verify password
const isMatch = await bcrypt.compare("myPassword", hashedPassword);
// Result: true
Complete example:
async function registerUser(email, password) {
// Hash the password
const saltRounds = 12; // Recommended 2024
const hashedPassword = await bcrypt.hash(password, saltRounds);
// Save to database
await db.users.insert({
email,
password: hashedPassword, // Store hash, never plaintext!
});
}
async function loginUser(email, password) {
// Get user
const user = await db.users.findOne({ email });
if (!user) throw new Error("User not found");
// Verify password
const isMatch = await bcrypt.compare(password, user.password);
if (!isMatch) throw new Error("Invalid password");
return user;
}
Python
import bcrypt
# Hash password
password = b"myPassword"
salt = bcrypt.gensalt(rounds=12)
hashed = bcrypt.hashpw(password, salt)
# Result: b'$2b$12$...'
# Verify password
is_match = bcrypt.checkpw(password, hashed)
# Result: True
PHP
// Hash password
$password = 'myPassword';
$hashed = password_hash($password, PASSWORD_BCRYPT, ['cost' => 12]);
// Verify password
if (password_verify($password, $hashed)) {
// Password matches
}
Choosing the Right Cost Factor
Cost factor determines how many iterations run:
| Cost Factor | Iterations | Time (2024) | Recommendation |
|---|---|---|---|
| 4 | 16 | < 0.01s | β Too fast |
| 8 | 256 | ~0.1s | β οΈ Acceptable |
| 10 | 1,024 | ~0.3s | β Standard |
| 12 | 4,096 | ~1s | β Recommended 2024 |
| 14 | 16,384 | ~4s | β οΈ May be too slow |
Recommendation 2024: Cost factor 12
Increase Cost Factor Over Time
// When user logs in, rehash with higher cost
async function rehashIfNeeded(user) {
const currentCost = extractCostFactor(user.password);
const recommendedCost = 12;
if (currentCost < recommendedCost) {
const newHash = await bcrypt.hash(user.plainPassword, recommendedCost);
await db.users.update(user.id, { password: newHash });
}
}
Best Practices
β Do This
-
Use at least cost factor 10-12
const hashed = await bcrypt.hash(password, 12); -
Always verify with compare()
const isValid = await bcrypt.compare(inputPassword, storedHash); -
Rehash periodically
- Increase cost factor as hardware gets faster
- Rehash when users log in
-
Combine with other security measures
- Rate limiting on login
- 2FA for sensitive accounts
- Password strength requirements
β Donβt Do This
-
Never store passwords in plain text
// β NEVER! await db.users.insert({ password: plainText }); -
Donβt use fast hash functions
// β NEVER! const hash = sha256(password); -
Donβt create custom hashing logic
// β NEVER! function myHash(password) { /* custom logic */ } -
Donβt expose cost factor
- Bcrypt hash contains cost factor (OK)
- But donβt expose implementation details
Security Considerations
Salt Handling
Bcrypt handles salt automatically:
- Each hash has unique salt
- Salt is included in hash string
- No need for separate salt storage
const hash1 = await bcrypt.hash("password", 10);
// $2a$10$salt1...hash1
const hash2 = await bcrypt.hash("password", 10);
// $2a$10$salt2...hash2 (different salt, different hash!)
// Both verify correctly:
await bcrypt.compare("password", hash1); // true
await bcrypt.compare("password", hash2); // true
Timing Attacks
bcrypt.compare() is not fully secure against timing attacks:
// β οΈ Possible timing attack
const user = await db.users.findOne({ email });
if (!user) throw new Error("User not found");
const isValid = await bcrypt.compare(password, user.password);
// β
Better: Even timing regardless of user existence
const user = await db.users.findOne({ email });
const hash = user?.password || "$2a$10$dummy";
const isValid = await bcrypt.compare(password, hash);
if (!user || !isValid) throw new Error("Invalid credentials");
Verify Your Implementations
Use our tools:
- Bcrypt Hash Generator - Test and generate hashes
- Password Strength Checker - Verify password strength
Migration from Unsafe Methods
Step 1: Identify Old Hashes
function isOldHash(hash) {
// MD5: 32 hex chars
if (/^[a-f0-9]{32}$/i.test(hash)) return true;
// SHA-256: 64 hex chars
if (/^[a-f0-9]{64}$/i.test(hash)) return true;
// bcrypt: starts with $2
if (/^\$2[ayb]\$/.test(hash)) return false;
return true; // Unknown format
}
Step 2: Rehash on Login
async function login(email, password) {
const user = await db.users.findOne({ email });
if (!user) throw new Error("Invalid credentials");
let isValid = false;
if (isOldHash(user.password)) {
// Verify against old hash
isValid = sha256(password) === user.password;
if (isValid) {
// Rehash with bcrypt
const newHash = await bcrypt.hash(password, 12);
await db.users.update(user.id, { password: newHash });
}
} else {
// Verify with bcrypt
isValid = await bcrypt.compare(password, user.password);
}
if (!isValid) throw new Error("Invalid credentials");
return user;
}
Conclusion
Bcrypt is the industry standard for password hashing for good reasons:
- Secure: Adaptive and slow design
- Proven: Used for over 20 years
- Simple: Automatic salt handling
- Scalable: Cost factor can be increased
When to use bcrypt:
- β Password hashing
- β API keys
- β Session tokens
- β Sensitive secrets
When to use something else:
- SHA-256 for file verification
- Argon2 for new projects (even more secure)
Next Steps
- Try Bcrypt Hash Generator
- Check Password Strength Checker
- Learn more about Hash Functions