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.