Scrypt: Memory-Hard Password Hashing

Scrypt: Memory-Hard Password Hashing

Scrypt, created by Colin Percival in 2009, introduced memory-hard password hashing to combat parallel attacks. While bcrypt resists GPU acceleration, scrypt goes further by requiring significant memory, making ASIC implementation prohibitively expensive. The algorithm's memory requirements scale with the work factor, forcing attackers to choose between slow sequential computation or expensive parallel hardware.

The algorithm operates in three phases: initial key derivation using PBKDF2, memory-intensive mixing using the ROMix algorithm, and final key derivation. The ROMix phase creates and randomly accesses a large memory buffer, ensuring that shortcuts require proportional memory. This memory-hard property fundamentally changes the economics of password cracking, as memory costs dominate over computational costs.

import scrypt
import time
import os

def demonstrate_scrypt():
    """Demonstrate scrypt usage and memory-hard properties"""
    
    password = b"SecurePassword123!"
    
    # Scrypt parameters: N (cost), r (block size), p (parallelization)
    print("Scrypt Parameter Effects:\n")
    
    # Vary N (memory cost)
    print("Varying N (memory cost):")
    for n_exp in range(10, 16):
        n = 2 ** n_exp
        r, p = 8, 1
        
        start = time.time()
        salt = os.urandom(16)
        key = scrypt.hash(password, salt, N=n, r=r, p=p, dklen=32)
        elapsed = time.time() - start
        
        memory_mb = (n * r * 128) / (1024 * 1024)
        print(f"N=2^{n_exp:2d} ({n:6d}): {elapsed:6.3f}s, ~{memory_mb:5.1f}MB - {key[:16].hex()}...")
    
    # Demonstrate proper usage with salt
    print("\nProper Scrypt Usage:")
    
    def hash_password(password):
        """Hash password with scrypt using secure parameters"""
        salt = os.urandom(16)
        n, r, p = 16384, 8, 1  # Recommended minimums for 2024
        key = scrypt.hash(password.encode('utf-8'), salt, N=n, r=r, p=p)
        
        # Store salt with hash for verification
        return salt + key
    
    def verify_password(password, stored):
        """Verify password against stored hash"""
        salt = stored[:16]
        stored_key = stored[16:]
        
        n, r, p = 16384, 8, 1  # Must match hashing parameters
        computed_key = scrypt.hash(password.encode('utf-8'), salt, N=n, r=r, p=p)
        
        # Constant-time comparison
        return secrets.compare_digest(computed_key, stored_key)
    
    # Example usage
    stored_hash = hash_password("MySecurePassword")
    print(f"Stored hash: {stored_hash.hex()[:32]}...")
    print(f"Verification (correct): {verify_password('MySecurePassword', stored_hash)}")
    print(f"Verification (wrong): {verify_password('WrongPassword', stored_hash)}")
    
    # Memory-hardness demonstration
    print("\nMemory-Hardness Impact:")
    print("Traditional GPU: 10,000 SHA-256 hashes/second/core")
    print("Traditional GPU: 100 scrypt hashes/second/core (memory limited)")
    print("Memory requirement prevents massive parallelization!")

demonstrate_scrypt()

Scrypt's parameters require careful tuning. The N parameter (CPU/memory cost) should be the highest value tolerable for your use case. The r parameter (block size) affects memory usage per hash. The p parameter (parallelization) allows trading computation for memory. Typical web applications might use N=16384, r=8, p=1, while high-security applications could use N=1048576 or higher.