Secure Password Storage: Best Practices with Modern Hashing Algorithms
Password security is paramount. Lets explore best practices for secure password storage, including use of robust hashing algorithms like bcrypt, scrypt,
Table of Contents
- Introduction
- Understanding Password Storage Fundamentals
- Modern Password Hashing Algorithms
- Implementation Best Practices
- Common Vulnerabilities and Mitigation
- Performance Considerations
- Migration Strategies
- Compliance and Regulatory Requirements
- Conclusion
Introduction
Secure password storage remains one of the most critical aspects of application security. Despite advances in passwordless authentication and biometrics, password-based authentication continues to be the primary method of user verification for most applications. This article provides a comprehensive guide to implementing secure password storage using modern hashing algorithms.
Understanding Password Storage Fundamentals
The Evolution of Password Storage
- Plain text storage (historically)
- Simple hashing (MD5, SHA-1)
- Salted hashes
- Modern adaptive hashing functions
Key Concepts
What Makes Password Storage Secure?
- One-way transformation: Impossible to reverse the hash to obtain the original password
- Uniqueness: Different passwords should produce different hashes
- Avalanche effect: Small changes in input create significant changes in output
- Computational intensity: Resistance to brute-force attacks
The Role of Salt
A salt is a random value that is:
- Unique for each user
- At least 16 bytes in length
- Generated using a cryptographically secure random number generator
- Stored alongside the password hash
import os
import hashlib
def generate_salt():
return os.urandom(16) # Generate a 16-byte random salt
Modern Password Hashing Algorithms
Comparison of Modern Algorithms
Argon2
Winner of the Password Hashing Competition (PHC)
from argon2 import PasswordHasher
ph = PasswordHasher(
time_cost=2, # Number of iterations
memory_cost=65536, # Memory usage in KiB
parallelism=4, # Number of parallel threads
hash_len=32, # Length of the hash in bytes
salt_len=16 # Length of the salt in bytes
)
hash = ph.hash("user_password")
Key Parameters:
- memory_cost: Amount of memory required
- time_cost: Number of iterations
- parallelism: Degree of parallelization
- hash_len: Length of the hash
- salt_len: Length of the salt
bcrypt
Industry Standard Since 1999
import bcrypt
def hash_password(password):
salt = bcrypt.gensalt(rounds=12) # Work factor of 2^12
return bcrypt.hashpw(password.encode(), salt)
Key Features:
- Built-in salt generation
- Configurable work factor
- Memory-hard function
- Wide platform support
scrypt
Memory-Hard Algorithm
import hashlib
import os
def hash_password_scrypt(password):
salt = os.urandom(16)
return hashlib.scrypt(
password.encode(),
salt=salt,
n=2**14, # CPU/memory cost
r=8, # Block size
p=1, # Parallelization
maxmem=2**25 # 32 MB
)
Algorithm Selection Guide
| Algorithm | Memory Cost | CPU Cost | Parallelism | Best Use Case |
|---|---|---|---|---|
| Argon2id | High | Medium | Yes | General purpose, high-security systems |
| bcrypt | Medium | High | No | Legacy systems, wide compatibility |
| scrypt | High | Medium | Limited | Memory-constrained environments |
Implementation Best Practices
1. Proper Salt Management
def create_hash_with_salt(password):
salt = generate_salt()
# Store both salt and hash
return {
'salt': salt.hex(),
'hash': hash_function(password, salt).hex()
}
2. Secure Parameter Selection
# Argon2 recommended parameters for different scenarios
ARGON2_PARAMETERS = {
'high_security': {
'time_cost': 4,
'memory_cost': 131072, # 128 MB
'parallelism': 4
},
'web_application': {
'time_cost': 2,
'memory_cost': 65536, # 64 MB
'parallelism': 2
},
'resource_constrained': {
'time_cost': 1,
'memory_cost': 32768, # 32 MB
'parallelism': 1
}
}
3. Hash Verification
def verify_password(stored_password_data, provided_password):
try:
salt = bytes.fromhex(stored_password_data['salt'])
stored_hash = bytes.fromhex(stored_password_data['hash'])
calculated_hash = hash_function(provided_password, salt)
return hmac.compare_digest(stored_hash, calculated_hash)
except Exception:
return False
Common Vulnerabilities and Mitigation
1. Rainbow Table Attacks
Mitigation: Proper salt implementation and modern algorithms
2. Timing Attacks
# Use constant-time comparison
from hmac import compare_digest
def secure_compare(a, b):
return compare_digest(a, b)
3. Brute Force Protection
from datetime import datetime, timedelta
class LoginAttemptTracker:
def __init__(self, max_attempts=5, lockout_period=timedelta(minutes=15)):
self.attempts = {}
self.max_attempts = max_attempts
self.lockout_period = lockout_period
def is_locked_out(self, user_id):
if user_id not in self.attempts:
return False
attempts = self.attempts[user_id]
if len(attempts) < self.max_attempts:
return False
latest_attempts = sorted(attempts)[-self.max_attempts:]
oldest_recent_attempt = latest_attempts[0]
return datetime.now() - oldest_recent_attempt < self.lockout_period
def record_attempt(self, user_id):
if user_id not in self.attempts:
self.attempts[user_id] = []
self.attempts[user_id].append(datetime.now())
Performance Considerations
Benchmarking Different Algorithms
import time
import statistics
def benchmark_hash_function(hash_func, iterations=100):
times = []
for _ in range(iterations):
start = time.perf_counter()
hash_func("test_password")
end = time.perf_counter()
times.append(end - start)
return {
'mean': statistics.mean(times),
'median': statistics.median(times),
'std_dev': statistics.stdev(times)
}
Optimal Parameter Selection
- Target hashing time: 250ms for authentication
- Scale parameters based on server capabilities
- Regular benchmark testing
Migration Strategies
Gradual Password Rehashing
def migrate_password_hash(old_hash_data, new_hash_function):
def wrapper(password):
# Verify with old hash
if verify_old_hash(old_hash_data, password):
# Generate new hash
new_hash = new_hash_function(password)
# Return new hash and migration flag
return new_hash, True
return None, False
return wrapper
Version Tracking
class PasswordHashData:
def __init__(self, version, salt, hash):
self.version = version
self.salt = salt
self.hash = hash
def to_dict(self):
return {
'version': self.version,
'salt': self.salt.hex(),
'hash': self.hash.hex()
}
@classmethod
def from_dict(cls, data):
return cls(
version=data['version'],
salt=bytes.fromhex(data['salt']),
hash=bytes.fromhex(data['hash'])
)
Compliance and Regulatory Requirements
NIST Guidelines (SP 800-63B)
- Minimum length: 8 characters
- Maximum length: At least 64 characters
- Allow all ASCII characters
- Implement rate limiting
- Use approved algorithms (Argon2, PBKDF2, scrypt, bcrypt)
GDPR Considerations
- Implement appropriate technical measures
- Document hashing procedures
- Regular security assessments
- Incident response planning
Conclusion
Secure password storage is a critical component of application security. By following these best practices:
- Use modern hashing algorithms (preferably Argon2id)
- Implement proper salting
- Choose appropriate work factors
- Regular security audits
- Plan for algorithm migration
- Monitor performance impacts
- Follow compliance requirements
You can create a robust password storage system that protects your users' credentials while maintaining system performance and usability.
References
Get the newsletter
New writing on identity, AI security, and building software, delivered when it ships. No tracking pixels, no funnels, unsubscribe with one click.