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.