How to Hash Passwords Securely
Never Use MD5 or SHA256 for Password Storage
This is the most important rule in password security: general-purpose hash functions like MD5, SHA1, and SHA256 should never be used for password storage. The reason is they're designed to be too fast โ capable of calculating billions of hashes per second. This means after obtaining hashes, attackers can find original passwords through brute force or dictionary attacks in very short time. In the 2012 LinkedIn data breach, about 60% of SHA1 password hashes were cracked within days because many users used weak passwords.
Special Requirements for Password Hashing
Secure password storage needs to meet three core requirements:
- Slow (Cost Factor): Hash computation should be deliberately designed to be slow (typically tens to hundreds of milliseconds), making brute force expensive
- Salt: Each password should use a unique random salt, preventing rainbow table attacks and ensuring identical passwords produce different hashes
- Adaptive: As hardware gets faster, the cost factor can be increased to maintain security
bcrypt: The Classic Password Hash Function
bcrypt was designed by Niels Provos and David Maziรจres in 1999 and is the most widely used password hash function today. It has built-in salt and cost factor, controlling computational complexity through the cost parameter:
// Node.js (using bcryptjs or bcrypt package)
const bcrypt = require('bcryptjs');
// Hashing a password
async function hashPassword(password) {
const saltRounds = 12; // Cost factor: 2^12 = 4096 iterations
const hash = await bcrypt.hash(password, saltRounds);
return hash;
// Returns: "$2b$12$[22-char salt][31-char hash]"
}
// Verifying a password
async function verifyPassword(password, hash) {
const match = await bcrypt.compare(password, hash);
return match; // true or false
}
// Usage
const hash = await hashPassword('mypassword123');
// Store hash in database
const isValid = await verifyPassword('mypassword123', hash);
// true
Cost parameter recommendation: For most applications, 12โ14 is appropriate (making hash time approximately 100โ300ms). As hardware improves, gradually increase this value.
Argon2: The Modern First Choice for Password Hashing
Argon2 won the 2015 Password Hashing Competition (PHC) and is considered the most secure password hashing algorithm today. Argon2 has three variants: Argon2i (resistant to side-channel attacks), Argon2d (resistant to GPU attacks), and Argon2id (combination of both, recommended). Compared to bcrypt, Argon2 can also configure memory usage, making it harder to crack in parallel with specialized hardware (ASIC).
// Python (using argon2-cffi)
from argon2 import PasswordHasher
ph = PasswordHasher(
time_cost=2, # Iterations
memory_cost=65536, # 64 MB RAM usage
parallelism=2, # Parallel threads
hash_len=32, # Hash output length
salt_len=16 # Salt length
)
# Hash
hash = ph.hash("mypassword123")
# Returns: "$argon2id$v=19$m=65536,t=2,p=2$..."
# Verify
try:
ph.verify(hash, "mypassword123")
print("Password correct")
except:
print("Password incorrect")
PBKDF2: The Widely Supported Standard
PBKDF2 (Password-Based Key Derivation Function 2) is a NIST-recommended and FIPS 140-2 approved password hashing scheme. While not as secure as bcrypt and Argon2 (no memory complexity), it's very useful in environments requiring compliance certification:
// Node.js (built-in crypto module)
const crypto = require('crypto');
const util = require('util');
const pbkdf2 = util.promisify(crypto.pbkdf2);
async function hashPassword(password) {
const salt = crypto.randomBytes(32).toString('hex');
const hash = await pbkdf2(password, salt, 310000, 32, 'sha256');
return `${salt}:${hash.toString('hex')}`;
}
async function verifyPassword(password, storedHash) {
const [salt, hash] = storedHash.split(':');
const newHash = await pbkdf2(password, salt, 310000, 32, 'sha256');
return crypto.timingSafeEqual(Buffer.from(hash, 'hex'), newHash);
}
Timing-Safe Comparison
When verifying passwords (or any secret values), you must use a timing-safe (constant-time) comparison function, not regular string equality (like === or ==). Regular string comparison returns at the first mismatching character, allowing attackers to infer hash content by measuring response times (timing attack). Node.js provides crypto.timingSafeEqual(), and bcrypt's compare() also has built-in timing-safe comparison.
Password Hashing Best Practices Summary
- Use Argon2id (first choice), bcrypt (second choice), or PBKDF2 (compliance scenarios)
- Never use MD5, SHA1, or SHA256 alone to hash passwords
- Never implement password hashing algorithms yourself โ use audited libraries
- Never store plaintext passwords, and never store encrypted passwords (encryption means they can be decrypted)
- Target hash computation time of approximately 100โ300ms (not too slow for users, but expensive to brute-force)
Try the free tool now
Use Free Tool โ