Bcrypt is one of the most trusted password hashing algorithms in the industry, designed specifically to protect user credentials against brute-force attacks. Unlike fast hashing algorithms like MD5 or SHA-256, Bcrypt is intentionally slow and incorporates built-in salt generation, making it ideal for secure password storage. This comprehensive guide explores how Bcrypt works, how to configure it properly, and best practices for implementation.
Table of Contents
- Key Takeaways
- What is Bcrypt
- How Bcrypt Works
- Understanding the Cost Factor
- Bcrypt Hash Structure Breakdown
- Bcrypt vs Other Algorithms
- Code Examples
- Security Best Practices
- FAQ
- Conclusion
Key Takeaways
- Built-in Salt: Bcrypt automatically generates a unique 128-bit salt for each password, eliminating the need for manual salt management.
- Adaptive Cost Factor: The work factor (rounds) can be increased over time to keep pace with hardware improvements.
- Slow by Design: Bcrypt is intentionally computationally expensive, making brute-force attacks impractical.
- Hash Structure: A Bcrypt hash contains the algorithm version, cost factor, salt, and hash in a single string.
- Recommended Cost Factor: Use a cost factor of 10-12 for most applications (targeting ~100ms-300ms hash time).
- Industry Standard: Bcrypt remains a solid choice for password hashing, though Argon2 is recommended for new projects.
Need to generate or verify Bcrypt hashes? Try our free online tool:
What is Bcrypt?
Bcrypt is a password hashing function designed by Niels Provos and David Mazières in 1999, based on the Blowfish cipher. The name "bcrypt" comes from "Blowfish crypt," reflecting its cryptographic foundation.
Why Bcrypt Was Created
Traditional hash functions like MD5 and SHA-1 were designed to be fast, which is great for data integrity checks but terrible for password storage. A fast hash means attackers can try billions of password guesses per second.
Bcrypt solves this by being:
- Deliberately Slow: Each hash computation takes significant time
- Configurable: The slowness can be adjusted via the cost factor
- Salt-Integrated: Each password gets a unique salt automatically
Key Features
| Feature | Description |
|---|---|
| Algorithm | Based on Blowfish cipher (Eksblowfish) |
| Output Size | 184 bits (24 bytes) hash + 128 bits salt |
| Salt | 128-bit, automatically generated |
| Cost Factor | Configurable (4-31), exponential work increase |
| String Format | $2a$, $2b$, or $2y$ prefix |
How Bcrypt Works
Bcrypt's security comes from its unique approach to password hashing. Here's the process:
Step-by-Step Process
- Salt Generation: A cryptographically secure 128-bit random salt is generated
- Key Setup: The password and salt are used to initialize the Eksblowfish cipher
- Expensive Key Schedule: The key schedule is repeated 2^cost times
- Encryption: A magic value ("OrpheanBeholderScryDoubt") is encrypted 64 times
- Output: The salt and resulting hash are combined into the final string
Why Same Password Produces Different Hashes
A common question is: "Why does hashing the same password twice give different results?"
This is because Bcrypt generates a new random salt for each hash operation. The salt is then embedded in the output string, so verification can extract it and reproduce the same hash.
Password: "mypassword"
First hash: $2b$10$N9qo8uLOickgx2ZMRZoMy.MqrqQb9lYz6H8Kj7OvBOyj5uYjiPWmu
Second hash: $2b$10$ZGdlbGVwaGFudHNhcmVjb.7xJ8L9KjQvMnOpRsTuVwXyZaBcDeFgH
Both are valid hashes of "mypassword" - different salts, same password!
Understanding the Cost Factor
The cost factor (also called "rounds" or "work factor") determines how computationally expensive the hashing process is. It's expressed as a power of 2.
Cost Factor Impact
| Cost Factor | Iterations | Approximate Time* |
|---|---|---|
| 4 | 16 | ~1ms |
| 8 | 256 | ~10ms |
| 10 | 1,024 | ~100ms |
| 12 | 4,096 | ~300ms |
| 14 | 16,384 | ~1s |
| 16 | 65,536 | ~4s |
*Times are approximate and vary by hardware
Choosing the Right Cost Factor
Recommendations:
- Development/Testing: Cost factor 4-6 (fast iteration)
- Production (Standard): Cost factor 10-12 (100-300ms)
- High Security: Cost factor 13-14 (if UX allows)
Upgrading Cost Factor Over Time
As hardware improves, you should increase the cost factor. Here's a strategy:
// Check if hash needs upgrading during login
async function loginAndUpgrade(password, storedHash) {
const isValid = await bcrypt.compare(password, storedHash);
if (isValid) {
const currentCost = parseInt(storedHash.split('$')[2]);
const targetCost = 12;
if (currentCost < targetCost) {
// Rehash with higher cost factor
const newHash = await bcrypt.hash(password, targetCost);
await updateUserHash(newHash);
}
}
return isValid;
}
Bcrypt Hash Structure Breakdown
A Bcrypt hash string contains all the information needed for verification:
$2b$12$N9qo8uLOickgx2ZMRZoMyeKj7OvBOyj5uYjiPWmuabcdefghijk
│ │ │ │ │
│ │ │ │ └── Hash (31 characters)
│ │ │ └── Salt (22 characters)
│ │ └── Cost Factor (2 digits)
│ └── Algorithm Version
└── Prefix Marker
Algorithm Versions
| Version | Description |
|---|---|
$2$ |
Original specification (obsolete) |
$2a$ |
Fixed bugs, most common |
$2b$ |
Fixed unsigned char bug (2014) |
$2y$ |
PHP-specific, equivalent to $2b$ |
Decoding a Real Hash
Let's break down this hash: $2b$10$vI8aWBnW3fID.ZQ4/zo1G.q1lRps.9cGLcZEiGDMVr5yUP1KUOYTa
| Component | Value | Meaning |
|---|---|---|
| Algorithm | $2b$ |
Bcrypt version 2b |
| Cost | 10 |
2^10 = 1,024 iterations |
| Salt | vI8aWBnW3fID.ZQ4/zo1G. |
22-char Base64 encoded salt |
| Hash | q1lRps.9cGLcZEiGDMVr5yUP1KUOYTa |
31-char Base64 encoded hash |
Bcrypt vs Other Algorithms
Comparison Table
| Feature | Bcrypt | Argon2 | Scrypt | PBKDF2 |
|---|---|---|---|---|
| Year | 1999 | 2015 | 2009 | 2000 |
| Memory Hard | No | Yes | Yes | No |
| GPU Resistant | Moderate | High | High | Low |
| Parallelism | No | Configurable | No | No |
| OWASP Recommended | ✅ | ✅ (Preferred) | ✅ | ✅ |
| Maturity | High | Medium | High | High |
When to Use Each
Summary:
- New projects: Consider Argon2id (if library support exists)
- Existing projects: Bcrypt is still excellent
- Legacy systems: Migrate from MD5/SHA to Bcrypt
- Resource-constrained: Bcrypt (lower memory requirements)
Code Examples
Node.js (bcryptjs)
const bcrypt = require('bcryptjs');
// Generate hash
async function hashPassword(password) {
const saltRounds = 12;
const hash = await bcrypt.hash(password, saltRounds);
return hash;
}
// Verify password
async function verifyPassword(password, hash) {
const isMatch = await bcrypt.compare(password, hash);
return isMatch;
}
// Usage
async function main() {
const password = 'mySecurePassword123';
// Hash the password
const hash = await hashPassword(password);
console.log('Hash:', hash);
// Output: $2a$12$LQv3c1yqBWVHxkd0LHAkCOYz6TtxMQJqhN8/X4.G5e0oO0oa5m6Wy
// Verify correct password
const isValid = await verifyPassword(password, hash);
console.log('Valid:', isValid); // true
// Verify wrong password
const isInvalid = await verifyPassword('wrongPassword', hash);
console.log('Invalid:', isInvalid); // false
}
main();
Python
import bcrypt
def hash_password(password: str) -> bytes:
"""Hash a password with bcrypt."""
salt = bcrypt.gensalt(rounds=12)
hashed = bcrypt.hashpw(password.encode('utf-8'), salt)
return hashed
def verify_password(password: str, hashed: bytes) -> bool:
"""Verify a password against a hash."""
return bcrypt.checkpw(password.encode('utf-8'), hashed)
# Usage
password = "mySecurePassword123"
# Hash
hashed = hash_password(password)
print(f"Hash: {hashed.decode()}")
# Verify
is_valid = verify_password(password, hashed)
print(f"Valid: {is_valid}") # True
is_invalid = verify_password("wrongPassword", hashed)
print(f"Invalid: {is_invalid}") # False
Java (Spring Security)
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
public class BcryptExample {
public static void main(String[] args) {
// Create encoder with strength (cost factor) 12
BCryptPasswordEncoder encoder = new BCryptPasswordEncoder(12);
String password = "mySecurePassword123";
// Hash the password
String hash = encoder.encode(password);
System.out.println("Hash: " + hash);
// Verify password
boolean isValid = encoder.matches(password, hash);
System.out.println("Valid: " + isValid); // true
boolean isInvalid = encoder.matches("wrongPassword", hash);
System.out.println("Invalid: " + isInvalid); // false
}
}
Go
package main
import (
"fmt"
"golang.org/x/crypto/bcrypt"
)
func hashPassword(password string) (string, error) {
bytes, err := bcrypt.GenerateFromPassword([]byte(password), 12)
return string(bytes), err
}
func verifyPassword(password, hash string) bool {
err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password))
return err == nil
}
func main() {
password := "mySecurePassword123"
// Hash
hash, _ := hashPassword(password)
fmt.Println("Hash:", hash)
// Verify
isValid := verifyPassword(password, hash)
fmt.Println("Valid:", isValid) // true
isInvalid := verifyPassword("wrongPassword", hash)
fmt.Println("Invalid:", isInvalid) // false
}
Security Best Practices
Do's ✅
- Use cost factor 10-12 for production systems
- Store the complete hash string (includes salt and version)
- Use constant-time comparison (built into bcrypt libraries)
- Upgrade cost factor as hardware improves
- Use HTTPS when transmitting passwords
- Implement rate limiting on login endpoints
Don'ts ❌
- Don't use cost factor below 10 in production
- Don't store salt separately (it's in the hash)
- Don't truncate passwords before hashing
- Don't use bcrypt for non-password data (use SHA-256)
- Don't log passwords or hashes in plain text
- Don't implement bcrypt yourself (use established libraries)
Password Length Considerations
Bcrypt has a maximum input length of 72 bytes. For longer passwords:
// Pre-hash long passwords with SHA-256
const crypto = require('crypto');
const bcrypt = require('bcryptjs');
async function hashLongPassword(password) {
// Pre-hash if password might exceed 72 bytes
const preHash = crypto
.createHash('sha256')
.update(password)
.digest('base64');
return bcrypt.hash(preHash, 12);
}
FAQ
Q1: Why does the same password produce different hashes?
Bcrypt generates a unique random salt for each hash operation. This salt is embedded in the output string. During verification, the salt is extracted and used to reproduce the hash. This design prevents rainbow table attacks.
Q2: What cost factor should I use?
For most applications, use cost factor 10-12, targeting 100-300ms hash time. Test on your production hardware and adjust accordingly. Higher security applications can use 13-14 if the UX impact is acceptable.
Q3: Is Bcrypt still secure in 2026?
Yes, Bcrypt remains secure and is recommended by OWASP. While Argon2 is preferred for new projects, Bcrypt with cost factor 10+ provides excellent protection against brute-force attacks.
Q4: Can I use Bcrypt for API keys or tokens?
No, Bcrypt is designed for password verification, not general hashing. For API keys or tokens, use SHA-256 or HMAC-SHA256. Bcrypt's slowness would create performance issues for high-frequency operations.
Q5: How do I migrate from MD5 to Bcrypt?
During user login, verify against MD5, then rehash with Bcrypt:
async function migrateHash(password, oldMd5Hash) {
const md5 = crypto.createHash('md5').update(password).digest('hex');
if (md5 === oldMd5Hash) {
// Password correct, create new bcrypt hash
const newHash = await bcrypt.hash(password, 12);
await updateUserHash(newHash);
return true;
}
return false;
}
Conclusion
Bcrypt remains one of the most reliable password hashing algorithms available. Its built-in salt generation, configurable cost factor, and battle-tested implementation make it an excellent choice for securing user credentials.
Key points to remember:
- Always use cost factor 10+ in production
- The hash string contains everything needed for verification
- Upgrade cost factor as hardware improves
- Consider Argon2 for new projects with memory-hard requirements
Ready to generate or verify Bcrypt hashes? Try our free online tool:
For more security tools, check out our Hash Generator and Password Generator.