Secure Password Storage: Best Practices with Modern Hashing Algorithms
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.