Implementing Salting Correctly
Implementing Salting Correctly
Proper salt implementation requires careful attention to generation, storage, and usage patterns. Each password must have its own unique salt, generated at the time of password creation or change. Reusing salts across passwords negates their security benefits, allowing attackers to identify users with identical passwords and potentially use partial rainbow tables.
import hashlib
import secrets
import struct
import base64
class SaltedPasswordHasher:
"""Demonstration of proper salt implementation"""
def __init__(self, salt_length=16, iterations=100000):
self.salt_length = salt_length
self.iterations = iterations
def hash_password(self, password):
"""Hash password with proper salting"""
if not password:
raise ValueError("Password cannot be empty")
# Generate cryptographically secure random salt
salt = secrets.token_bytes(self.salt_length)
# Use PBKDF2 for key stretching (better than plain SHA)
key = hashlib.pbkdf2_hmac(
'sha256',
password.encode('utf-8'),
salt,
self.iterations
)
# Store salt with hash for later verification
# Format: salt_length(1) + iterations(4) + salt(n) + key(32)
stored = struct.pack('!BI', self.salt_length, self.iterations)
stored += salt + key
# Encode for storage (base64 for text storage)
return base64.b64encode(stored).decode('ascii')
def verify_password(self, password, stored_hash):
"""Verify password against stored hash"""
try:
# Decode from storage format
stored = base64.b64decode(stored_hash.encode('ascii'))
# Extract components
salt_length, iterations = struct.unpack('!BI', stored[:5])
salt = stored[5:5+salt_length]
stored_key = stored[5+salt_length:]
# Compute key with same parameters
computed_key = hashlib.pbkdf2_hmac(
'sha256',
password.encode('utf-8'),
salt,
iterations
)
# Constant-time comparison
return secrets.compare_digest(computed_key, stored_key)
except Exception:
# Don't leak information about failures
return False
def demonstrate_salt_uniqueness(self):
"""Show that same password gets different hashes"""
password = "SamePassword123!"
print("Same password, different salts:")
for i in range(5):
hashed = self.hash_password(password)
# Decode to show internal structure
stored = base64.b64decode(hashed.encode('ascii'))
salt = stored[5:5+self.salt_length]
print(f"Hash {i+1}: {hashed[:20]}... Salt: {salt.hex()[:16]}...")
# Demonstration
hasher = SaltedPasswordHasher()
# Hash passwords
password1 = "MySecurePassword123!"
hash1 = hasher.hash_password(password1)
print(f"Hashed password: {hash1}")
# Verify correct password
assert hasher.verify_password(password1, hash1)
print("✓ Correct password verified")
# Verify wrong password fails
assert not hasher.verify_password("WrongPassword", hash1)
print("✓ Wrong password rejected")
# Demonstrate uniqueness
print("\n")
hasher.demonstrate_salt_uniqueness()
Storage format design impacts system maintainability and security. Storing the salt, algorithm parameters, and hash together enables future migrations and parameter updates. Many systems use a delimited format like $algorithm$params$salt$hash
or encode everything in a binary structure. This self-contained format ensures password verification remains possible even as security parameters evolve.