Why Password Hashing Requires Special Algorithms
Password Hashing: Making Cracking Prohibitively Expensive
Storing passwords securely is one of the most fundamental requirements of any authentication system. The goal is not to make passwords unbreakable — given enough time and resources, any hash can be cracked. The goal is to make cracking so expensive in time and hardware that it is impractical for an attacker.
Why Fast Hashes Fail for Passwords
General-purpose hash functions like MD5, SHA-1, and SHA-256 are designed for speed. They verify data integrity and compute checksums quickly. This speed is a feature for their intended use but a catastrophic flaw for password storage.
The math of cracking speed:
- A modern GPU can compute billions of SHA-256 hashes per second
- An 8-character password has ~218 trillion (95^8) possible combinations
- At 10 billion hashes/second, cracking all 8-character passwords takes ~6 hours
- MD5 is even faster — trillions of hashes per second on modern hardware
Real-world example: In 2012, LinkedIn stored passwords using unsalted SHA-1. Attackers cracked 6.5 million passwords within days using GPU clusters. The fast hash made brute-force attacks trivially cheap.
What Makes a Good Password Hash
A password hashing algorithm must have these properties:
Slow: Each hash computation should take 100-500ms on production hardware. This makes brute-force attacks impractical while remaining acceptable for legitimate login attempts.
Memory-hard: The algorithm should require significant memory (64+ MB) to compute. This makes GPU and ASIC attacks impractical because these devices have limited memory per core.
Salted: Each password must be hashed with a unique random salt. This prevents rainbow table attacks and ensures that identical passwords produce different hashes.
Adaptive: The algorithm should allow increasing the work factor as hardware improves, without breaking existing hashes.
The Two Leading Algorithms
Argon2 (winner of the Password Hashing Competition, 2015):
- Argon2id is the recommended variant (resists both GPU and side-channel attacks)
- Memory-hard: requires configurable memory (default 64 MB)
- Time-hard: configurable number of iterations (default 3)
- Parallelism: configurable threads (default 4)
- Resistant to GPU cracking because GPUs have limited memory per core
bcrypt (based on the Blowfish cipher, 1999):
- Time-hard: configurable cost factor (default 12, meaning 2^12 = 4,096 iterations)
- Not memory-hard: uses only 4 KB of memory
- Still widely used and considered secure with appropriate cost factors
- Limited to 72-byte passwords (longer passwords are truncated)
Salts: Why They Matter
A salt is a random value added to each password before hashing. The salt is stored alongside the hash (it is not a secret).
Without salts:
hash("password") = 5f4dcc3b5aa765d61d8327deb882cf99 (MD5)
Every user with the password "password" has the same hash. An attacker can precompute a rainbow table of common password hashes and instantly look up any matching hash.
With salts:
hash("password" + "salt_A") = 7c6a180b36896a0a8c02787eeafb0e4c
hash("password" + "salt_B") = 6cb75f652a9b52798eb6cf2200e8896c
Identical passwords produce different hashes. Rainbow tables are useless because the attacker would need a separate table for every possible salt.
Salt requirements:
- Unique per password (never reuse salts)
- Cryptographically random (use
os.urandomorsecrets) - Sufficiently long (16+ bytes)
- Stored alongside the hash (not a secret)
Implementing Argon2 and bcrypt
Practical Implementation of Password Hashing
Argon2id Implementation (Python)
from argon2 import PasswordHasher
from argon2.exceptions import VerifyMismatchError
# Create hasher with recommended parameters
ph = PasswordHasher(
time_cost=3, # Number of iterations
memory_cost=65536, # 64 MB in KiB
parallelism=4, # Number of threads
hash_len=32, # Length of the hash in bytes
salt_len=16 # Length of the salt in bytes
)
# Hash a password (automatically generates a random salt)
password = "user_password_123"
hash_string = ph.hash(password)
# Example output: $argon2id$v=19$m=65536,t=3,p=4$c29tZXNhbHQ$RdescudvJCsgt3ub+b+dWRWJTmaaJObG
# Verify a password
try:
ph.verify(hash_string, "user_password_123")
print("Password correct")
except VerifyMismatchError:
print("Password incorrect")
# Check if hash needs rehashing (parameters have changed)
if ph.check_needs_rehash(hash_string):
new_hash = ph.hash(password)
# Update stored hash in database
The encoded hash string contains all parameters and the salt, so verification does not require storing them separately.
bcrypt Implementation (Python)
import bcrypt
# Hash a password
password = b"user_password_123" # bcrypt requires bytes
salt = bcrypt.gensalt(rounds=12) # Cost factor 12 (2^12 = 4,096 iterations)
hash_bytes = bcrypt.hashpw(password, salt)
# Example output: b'$2b$12$LJ3m4ys5Lz5z5z5z5z5z5O...'
# Verify a password
if bcrypt.checkpw(b"user_password_123", hash_bytes):
print("Password correct")
else:
print("Password incorrect")
bcrypt Implementation (Node.js)
const bcrypt = require('bcrypt');
const SALT_ROUNDS = 12;
// Hash a password
const hash = await bcrypt.hash('user_password_123', SALT_ROUNDS);
// Verify a password
const match = await bcrypt.compare('user_password_123', hash);
if (match) {
console.log('Password correct');
}
Choosing Parameters
Argon2id parameters:
memory_cost: 64 MB (65536 KiB) is the recommended minimum for productiontime_cost: 3 iterations is the recommended minimumparallelism: 4 threads (adjust based on available CPU cores)- Target: 100-500ms per hash on production hardware
bcrypt cost factors:
- Cost 10: ~100ms (minimum acceptable)
- Cost 12: ~300ms (recommended for most applications)
- Cost 14: ~1s (high-security applications)
- Increase by 1 every 18-24 months as hardware improves
Benchmarking script:
import time
from argon2 import PasswordHasher
ph = PasswordHasher(time_cost=3, memory_cost=65536, parallelism=4)
start = time.time()
ph.hash("benchmark")
elapsed = time.time() - start
print(f"Hash time: {elapsed:.3f}s")
# Target: 0.1 - 0.5 seconds
Migration Strategy
When upgrading hashing parameters or migrating from a weaker algorithm:
- Do not force password resets — this creates poor user experience and does not improve security (users choose weaker passwords).
- Rehash on next login — when a user successfully logs in with an old hash, rehash with the new algorithm and update the database.
def verify_and_update(password, stored_hash):
if stored_hash.startswith('$argon2id$'):
# Current algorithm — verify normally
try:
ph.verify(stored_hash, password)
if ph.check_needs_rehash(stored_hash):
update_hash_in_db(ph.hash(password))
return True
except VerifyMismatchError:
return False
else:
# Legacy algorithm (e.g., bcrypt) — verify and upgrade
if bcrypt.checkpw(password.encode(), stored_hash.encode()):
update_hash_in_db(ph.hash(password))
return True
return False
Practice: Implement Secure Password Storage
Implement a secure password storage system for a new application. Choose the algorithm, parameters, and write the hash and verify functions.
Practice Problems
Your application currently uses bcrypt with cost factor 10. You want to migrate to Argon2id. Choose appropriate parameters and explain your reasoning. Your server has 4 CPU cores and 16 GB RAM, and you expect 100 concurrent login requests per minute.
Solution
Recommended Argon2id parameters:
- `memory_cost=65536` (64 MB) — the OWASP recommended minimum. With 100 concurrent logins, this uses 6.4 GB of memory, well within the 16 GB available.
- `time_cost=3` — the recommended minimum number of iterations.
- `parallelism=4` — matches the 4 CPU cores. Each hash uses 4 threads.
Benchmark to verify the hash time is 100-500ms. If it is too slow, reduce memory_cost to 32768 (32 MB). If it is too fast, increase time_cost to 4.
Migration strategy: verify existing bcrypt hashes on login, and rehash with Argon2id when the user next logs in. Do not force password resets. A breached database contains password hashes in the format `5f4dcc3b5aa765d61d8327deb882cf99`. Identify the hashing algorithm, explain why it is insecure, and describe how an attacker would crack these hashes.
Solution
The hash `5f4dcc3b5aa765d61d8327deb882cf99` is the MD5 hash of "password". MD5 is a fast, general-purpose hash function that is completely unsuitable for password storage because:
1. **Speed**: Modern GPUs can compute billions of MD5 hashes per second, making brute-force attacks trivially cheap.
2. **No salt**: The hash is unsalted, so identical passwords produce identical hashes. An attacker can use precomputed rainbow tables to instantly look up common passwords.
3. **Broken algorithm**: MD5 has known collision attacks and is cryptographically broken.
An attacker would:
1. Use a rainbow table (precomputed hash-to-password mapping) to instantly crack common passwords
2. For uncracked hashes, use GPU-accelerated brute-force tools like Hashcat, trying billions of candidates per second
3. Crack 8-character lowercase passwords in hours, 8-character mixed-case alphanumeric in days
The fix: rehash all passwords with Argon2id or bcrypt, using unique salts and appropriate work factors. Quiz
1. Why are fast hash functions like MD5 and SHA-256 unsuitable for password storage?
2. What is the primary advantage of Argon2id over bcrypt?
3. What is the purpose of a salt in password hashing?
4. What is the recommended approach when upgrading from bcrypt to Argon2id?
5. What is the recommended minimum memory cost for Argon2id in production?
Flashcards
Question
Why are MD5 and SHA-256 unsuitable for password storage?
Click to reveal answer
Answer
They are fast hash functions designed for data integrity. Their speed makes brute-force attacks cheap — a modern GPU can compute billions of hashes per second. Password hashing must be slow and memory-hard.
Question
What is Argon2id and why is it recommended?
Click to reveal answer
Answer
Argon2id is the winner of the Password Hashing Competition (2015). It is memory-hard (requires 64+ MB), time-hard (configurable iterations), and parallelizable. Memory-hardness makes GPU and ASIC attacks impractical.
Question
What is the purpose of a salt?
Click to reveal answer
Answer
A salt is a unique random value added to each password before hashing. It ensures identical passwords produce different hashes, making rainbow table attacks useless. Salts are stored alongside the hash and are not a secret.
Question
What is the recommended bcrypt cost factor for production?
Click to reveal answer
Answer
Cost factor 12 (2^12 = 4,096 iterations) is recommended for most applications, taking ~300ms per hash. Increase by 1 every 18-24 months as hardware improves.
Question
How should you migrate from bcrypt to Argon2id?
Click to reveal answer
Answer
Rehash on next login. When a user successfully authenticates with their bcrypt hash, verify it, then rehash the plaintext password with Argon2id and update the database. Do not force password resets.
Revision Notes
Key Takeaways
- 1. Fast hashes (MD5, SHA-256) are unsuitable for passwords — their speed makes brute-force attacks cheap
- 2. Argon2id is memory-hard (64+ MB), making GPU and ASIC attacks impractical
- 3. bcrypt is time-hard but not memory-hard — still acceptable with cost factor 12+
- 4. Salts ensure identical passwords produce different hashes, defeating rainbow tables
- 5. Migrate by rehashing on next login — never force password resets
Interview Tips
- • Explain why fast hashes fail for passwords with concrete cracking speed numbers
- • Compare Argon2id and bcrypt — focus on memory-hardness as the key differentiator
- • Describe the migration strategy from bcrypt to Argon2id (rehash on login)
- • Connect password hashing to the OWASP Top 10 (A02: Cryptographic Failures) and real-world breaches (LinkedIn 2012)
Cheat Sheet
Password Hashing with Argon2 and bcrypt Cheat Sheet
Phase: Password Storage and Session Security | Level: intermediate
Why Not Fast Hashes:
- MD5, SHA-256: billions of hashes/second on GPUs — cracking is trivial
- LinkedIn 2012: 6.5M passwords cracked in days using unsalted SHA-1
Argon2id (Recommended):
- Memory-hard: 64 MB minimum (65536 KiB)
- Time-hard: 3 iterations minimum
- Parallelism: 4 threads (match CPU cores)
- Target: 100-500ms per hash
- Resists GPU/ASIC attacks (limited memory per core)
bcrypt (Acceptable):
- Cost factor 12 (~300ms) recommended
- Not memory-hard (4 KB) — vulnerable to GPU acceleration
- Limited to 72-byte passwords
Salts:
- Unique per password, cryptographically random, 16+ bytes
- Stored alongside hash (not a secret)
- Prevents rainbow table attacks
Migration:
- Rehash on next login — never force password resets
- Check
needs_rehash()to upgrade parameters over time
Key points:
- Argon2 won the Password Hashing Competition for memory-hard resistance to GPU attacks
- bcrypt's cost factor raises computation time as hardware improves over the years
- Fast hashes like MD5 or SHA-256 are unsuitable for passwords because cracking is cheap
Tags: passwords, cryptography