scrypt is a password-based key derivation function that is designed to be resistant to hardware acceleration, making it more secure against brute-force attacks.
Let’s see scrypt in action. Imagine we’re hashing a user’s password, "mysecretpassword", for storage in a database.
import hashlib
import os
# Example parameters (these would typically be tuned for security)
N = 1024 # CPU/memory cost factor (must be a power of 2)
r = 8 # block mixing factor
p = 1 # parallelization factor
password = b"mysecretpassword"
salt = os.urandom(16) # Generate a random salt
# scrypt returns bytes
hashed_password = hashlib.scrypt(password, salt=salt, n=N, r=r, p=p)
print(f"Salt: {salt.hex()}")
print(f"Hashed Password: {hashed_password.hex()}")
When we run this, we might get output like:
Salt: 8a7b2c1d0e9f8a7b2c1d0e9f8a7b2c1d
Hashed Password: a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6
The salt is a random value unique to each password hash, preventing rainbow table attacks. The hashed_password is the result of scrypt’s computation, which is what we’d store. To verify a password, we’d retrieve the stored salt and hashed password, and then run hashlib.scrypt with the same parameters and the user’s entered password. If the resulting hash matches the stored hash, the password is correct.
The core problem scrypt addresses is the increasing prevalence of specialized hardware (like GPUs and ASICs) that can perform simple cryptographic hashes extremely quickly. Traditional hashing algorithms like SHA-256 are fast, making them vulnerable to brute-force attacks where an attacker can try billions of passwords per second. scrypt, by design, requires a significant amount of memory to compute its hash. This memory requirement is the key to its resistance against hardware acceleration. GPUs and ASICs are optimized for parallel computation, but they typically have limited on-chip memory. scrypt’s memory usage forces attackers to use more expensive and less efficient hardware, or to scale down their attack, thereby increasing the cost and time required to crack a password.
Internally, scrypt performs a series of computationally intensive operations, most notably a large number of random accesses to a large memory buffer. It starts with a pseudo-random function (PRF) and uses a salt and the password to generate an initial state. This state is then used to fill a large block of memory (controlled by the N parameter, which determines the memory size as 32 * N * r bytes). The algorithm then performs a sequence of "mix" operations, where it repeatedly reads from and writes to this memory buffer in a pseudo-random fashion. Each mix operation involves a lookup, a computation, and a write-back. The r parameter controls how many times each "pass" through the memory buffer is mixed, and the p parameter allows for parallelization of these mix operations. The final output is derived from the state of this memory buffer after all operations are complete.
The parameters N, r, and p are critical for scrypt’s security. N is the most important, directly controlling the memory cost. A higher N means more memory and more computation. r affects the mixing, and p allows for parallelization, which can speed up computation on multi-core CPUs but doesn’t help much with hardware accelerators that have limited memory bandwidth. Choosing these parameters is a trade-off between security and performance. For example, a common recommendation is N=2**14, r=8, p=1, but these should be tuned based on the expected hardware of an attacker and the acceptable latency for password verification.
What most people don’t realize is that the order in which the derived keys are used to populate and mix the memory buffer is not strictly sequential. The "parallelization" factor p actually influences how many independent sequences of operations are performed concurrently, and their results are then combined. This structure, combined with the pseudo-random access patterns, makes it very difficult to parallelize the computation efficiently on hardware that doesn’t have large, fast memory access. The memory is not just a large scratchpad; it’s actively and randomly probed and written to, making memory bandwidth a significant bottleneck.
The next step in secure password handling is often exploring password managers and two-factor authentication.