bcrypt, scrypt, and Argon2: Choosing the Right Password Hashing Algorithm
Table of Contents
- Introduction
- Understanding Password Hashing Requirements
- Algorithm Deep Dive
- bcrypt
- scrypt
- Argon2
- Comparative Analysis
- Implementation Considerations
- Real-world Usage Scenarios
- Performance Benchmarks
- Security Considerations
- Making the Right Choice
- Future Considerations
Introduction
Password hashing is a critical component of modern authentication systems. Unlike general-purpose hash functions like SHA-256, password hashing algorithms are specifically designed to be computationally intensive and memory-hard, making them resistant to various forms of attacks. This article provides a detailed technical analysis of three leading password hashing algorithms: bcrypt, scrypt, and Argon2.
Understanding Password Hashing Requirements
Before diving into specific algorithms, let's establish the key requirements for password hashing:
- Slow Computation: Unlike general-purpose hash functions, password hashing must be deliberately slow to prevent high-speed brute-force attacks.
- Salt Integration: The algorithm must incorporate cryptographic salts to prevent rainbow table attacks and ensure unique hashes for identical passwords.
- Memory Hardness: Resistance to hardware-based attacks (GPU, ASIC) through memory-intensive operations.
- Tunability: Ability to adjust computational and memory costs as hardware capabilities evolve.
Algorithm Deep Dive
bcrypt
bcrypt, derived from the Blowfish cipher, was introduced in 1999 by Niels Provos and David Mazières.
Technical Specifications:
bcrypt(password, salt, cost) =
EksBlowfishSetup(cost, salt, password) // Initialize Blowfish state
state = "OrpheanBeholderScryDoubt" // Initial state
for (i = 0; i < 64; i++)
state = ExpandKey(state, 0, salt) // Multiple rounds of Blowfish
state = ExpandKey(state, 0, password)
return state
Key Features:
- Cost factor (work factor) ranges from 4 to 31
- Fixed output size of 24 bytes
- Integrated salt handling (128-bit)
- Built-in version identification in output format
Advantages:
- Battle-tested (20+ years in production)
- Simple to implement correctly
- Built-in salt handling
- Adjustable work factor
Limitations:
- Fixed memory usage (~4KB)
- No memory hardness
- Maximum password length of 72 bytes
scrypt
scrypt, designed by Colin Percival in 2009, introduced memory hardness to password hashing.
Technical Parameters:
scrypt(password, salt, N, r, p) where:
N = CPU/Memory cost parameter
r = Block size parameter
p = Parallelization parameter
Core Functions:
def scrypt(password, salt, N, r, p):
# Initialize blocks
B = PBKDF2(password, salt)
# Memory-hard mixing function
V = ROMix(B, N)
# Final derivation
return PBKDF2(password, V)
def ROMix(B, N):
V = [0] * N
X = B
# Memory-filling loop
for i in range(N):
V[i] = X
X = BlockMix(X)
# Memory-dependent mixing
for i in range(N):
j = Integrify(X) % N
X = BlockMix(X ⊕ V[j])
return X
Key Features:
- Configurable memory usage
- Time-memory trade-off resistance
- Parallel processing support
- Sequential memory-hard operations
Argon2
Argon2, the winner of the Password Hashing Competition (2015), comes in three variants:
- Argon2d: Data-dependent memory access (fastest)
- Argon2i: Data-independent memory access (side-channel resistant)
- Argon2id: Hybrid approach (recommended default)
Core Parameters:
Argon2(P, S, m, t, p, v) where:
P = Password
S = Salt
m = Memory size
t = Number of iterations
p = Degree of parallelism
v = Version number
Implementation Example:
def argon2_hash(password, salt):
# Parameters for modern systems (2023)
memory_cost = 65536 # 64 MB
time_cost = 3 # 3 iterations
parallelism = 4 # 4 parallel threads
return Argon2id.hash(
password=password,
salt=salt,
memory_cost=memory_cost,
time_cost=time_cost,
parallelism=parallelism
)
Comparative Analysis
Feature | bcrypt | scrypt | Argon2 |
---|---|---|---|
Year | 1999 | 2009 | 2015 |
Memory Usage | Fixed (~4KB) | Configurable | Configurable |
Parallel Processing | No | Yes | Yes |
Side-Channel Resistance | Partial | No | Yes (Argon2i/id) |
GPU Resistance | Moderate | High | Very High |
Memory Hardness | No | Yes | Yes |
Implementation Complexity | Low | Medium | Medium |
Implementation Considerations
Parameter Selection Guidelines
Argon2:
# Modern (2023) recommended parameters
memory_cost = 65536 # 64 MB
time_cost = 3 # iterations
parallelism = 4 # threads
# Example implementation
def hash_password_argon2(password):
salt = os.urandom(16)
return argon2.hash_password(
password.encode(),
salt=salt,
time_cost=time_cost,
memory_cost=memory_cost,
parallelism=parallelism,
type=argon2.Type.ID
)
scrypt:
# Modern (2023) recommended parameters
N = 2**15 # CPU/Memory cost
r = 8 # Block size
p = 1 # Parallelization
# Example implementation
def hash_password_scrypt(password):
salt = os.urandom(16)
return scrypt.hash(
password,
salt=salt,
N=N,
r=r,
p=p,
buflen=32
)
bcrypt:
# Modern (2023) recommended parameters
work_factor = 12 # Targeting ~250ms hash time on modern hardware
# Example implementation
def hash_password_bcrypt(password):
salt = os.urandom(16)
return bcrypt.hashpw(password.encode(), salt, rounds=work_factor)
Real-world Usage Scenarios
1. High-Security Systems
Recommended: Argon2id
# High-security parameters
memory_cost = 131072 # 128 MB
time_cost = 4
parallelism = 4
2. Web Applications
Recommended: bcrypt or Argon2id
# Web-friendly parameters
bcrypt_work_factor = 12 # ~250ms
# or
argon2_memory = 32768 # 32 MB
argon2_time = 2
3. Resource-Constrained Systems
Recommended: bcrypt
# Resource-conscious parameters
work_factor = 10 # ~50ms
Performance Benchmarks
Performance measurements on a modern system (Intel i7, 16GB RAM):
bcrypt (work_factor=12):
- Hash time: ~250ms
- Memory usage: ~4KB
scrypt (N=2^15, r=8, p=1):
- Hash time: ~200ms
- Memory usage: ~32MB
Argon2id (m=65536, t=3, p=4):
- Hash time: ~150ms
- Memory usage: ~64MB
Security Considerations
- Attack Resistance:
- bcrypt: Resistant to GPU attacks through Blowfish's heavy key setup
- scrypt: Resistant to ASIC attacks through memory hardness
- Argon2: Comprehensive resistance including side-channel attacks
Common Vulnerabilities:
# DON'T: Using static salts
static_salt = b"fixed_salt" # Vulnerable
# DO: Generate unique salts
unique_salt = os.urandom(16) # Secure
Making the Right Choice
Decision Framework:
- Choose bcrypt if:
- You need a battle-tested solution
- Implementation simplicity is crucial
- Memory usage must be minimal
- Choose scrypt if:
- Memory-hardness is a primary concern
- You need flexible resource utilization
- You have control over hardware resources
- Choose Argon2 if:
- You want the most modern algorithm
- Side-channel resistance is required
- You need maximum flexibility in parameters
Future Considerations
- Quantum Computing Impact:
- Current password hashing algorithms remain quantum-resistant
- Focus on increasing parameter costs with hardware improvements
- Emerging Standards:
- NIST recommendations increasingly favor Argon2
- Industry trend toward memory-hard functions
- Hardware Evolution:
- Regular parameter adjustments needed
- Monitor hash timing to maintain security margins
Conclusion
For most modern applications, Argon2id represents the best choice due to its flexibility, security features, and ongoing development. However, bcrypt remains a solid, battle-tested alternative, especially when simplicity and limited resources are primary concerns. The key is to choose parameters appropriate for your specific use case and regularly review and update them as hardware capabilities evolve.
Remember: The security of any password hashing implementation depends not just on the algorithm choice, but on proper parameter selection, salt generation, and overall system design.