Skip to main content
Cipher Visualizers home

The scrypt Algorithm

scrypt is a password-based key derivation function that makes every guess cost memory as well as time. Learn how Salsa20/8, BlockMix, and ROMix fill and read a large table, how its parameters work, and what its security proofs say.

Pashalis Laoutaris 19 min read

Try it

Interactive scrypt Visualizer

🔐 scrypt

6
Enter text and click a button to start!
–

The scrypt Algorithm

Introduction

A password is a weak secret, so any system that turns one into a key must make every guess expensive. PBKDF2 does this with time. It repeats a hash thousands of times. An attacker with GPUs or custom chips can run millions of those hash chains at once, because each chain needs only a few bytes of memory.

scrypt adds memory to the cost. It fills a large table with pseudorandom data. Then it reads the table back in an order that depends on the data itself. A machine that wants to be fast must keep the whole table in memory. Memory costs about the same on every kind of hardware, so a custom chip loses most of its advantage. This guide covers every step of scrypt, from the Salsa20/8 core up to the last PBKDF2 call.

Table of Contents

History and Standards

Colin Percival designed scrypt for Tarsnap, an online backup service. He described it at BSDCan in May 2009, in a paper titled “Stronger Key Derivation via Sequential Memory-Hard Functions”. The IETF published it as RFC 7914 in August 2016, with Simon Josefsson as a second author. The RFC is informational.

The RFC says that ROMix has been proven sequential memory-hard under the random oracle model for the hash function. Later work went further. In 2016, Joel Alwen, Binyi Chen, Krzysztof Pietrzak, Leonid Reyzin, and Stefano Tessaro proved that scrypt is maximally memory-hard, in a stronger model that allows many parallel hash calls. Their paper also notes that scrypt is used in cryptocurrencies such as Litecoin and Dogecoin, and that it inspired Argon2d.

The Three Stages

scrypt takes a password P, a salt S, and three cost parameters N, r, and p. It also takes the length of the key that you want. It works in three stages:

  1. Spread. PBKDF2-HMAC-SHA-256 with one iteration turns P and S into p blocks of 128 * r bytes each.
  2. Mix. Each block goes through ROMix on its own. ROMix is the memory-hard part.
  3. Collect. PBKDF2-HMAC-SHA-256 with one iteration takes P as the password and all mixed blocks as the salt. Its output is the key.

The two PBKDF2 calls are a cheap wrapper. All of the cost is in stage 2. Because the p blocks are independent, they can be mixed in parallel. The inner pieces are a small hash-like function called Salsa20/8, a mixing function called BlockMix, and ROMix itself.

The Salsa20/8 Core

The core of the mixing is the Salsa20/8 core, which Daniel Bernstein designed as part of the Salsa20 stream cipher. It takes 64 bytes and returns 64 bytes. The bytes are read as sixteen 32-bit little-endian words, x0 to x15. The RFC points out that this core is not a cryptographic hash function, because it is not collision resistant. scrypt does not need that property.

The core uses only addition, rotation, and XOR. Its basic operation is the quarter round. For four words a, b, c, and d taken in this order, it computes:

b = b xor rol(a + d, 7)
c = c xor rol(b + a, 9)
d = d xor rol(c + b, 13)
a = a xor rol(d + c, 18)

Arrange the sixteen words as a 4 by 4 matrix. A column round applies a quarter round to each of the four columns, and each column starts from its diagonal word. A row round does the same for the four rows. The core runs four column rounds and four row rounds, one after the other, which makes 8 rounds in all. That is where the “8” in the name comes from. At the end, the original input words are added back to the result, word by word. This last addition stops the core from being a permutation that could be run backwards.

BlockMix

BlockMix works on a string of 2r blocks of 64 bytes. It makes every block depend on every other block by chaining the Salsa20/8 core over them:

X = B[2r - 1]
for i = 0 to 2r - 1:
    X = Salsa20/8( X xor B[i] )
    Y[i] = X
output = Y[0], Y[2], ..., Y[2r - 2], Y[1], Y[3], ..., Y[2r - 1]

The first block takes the last input block as its starting value. After that, each core call takes the output of the one before it. The last step shuffles the output blocks, so that the even-numbered ones come first and the odd-numbered ones come second. With r = 1, the input has two blocks, 128 bytes, and BlockMix makes two Salsa20/8 calls.

ROMix

ROMix is the memory-hard step. It takes a string B of 128 * r bytes and the cost N. It has two loops of N rounds each:

X = B
for i = 0 to N - 1:       # fill the table
    V[i] = X
    X = BlockMix(X)
for i = 0 to N - 1:       # read it back
    j = Integerify(X) mod N
    X = BlockMix( X xor V[j] )
output = X

Integerify reads the last 64-byte block of X as a little-endian number. Since N is a power of 2, only the low bits of that number matter for j mod N. When N is at most 2^32, the low 32 bits are enough, and the visualizer uses them. The Python code uses the whole number, as the RFC defines it. In the first loop, the table V is filled with a chain of N values, each one made from the one before. In the second loop, the next table entry to read, j, depends on the current X. So the order of reads cannot be known in advance. A computer that does not store V has to rebuild the entry from an earlier one. That costs extra BlockMix calls, and the section on security measures how many.

The table needs 128 * r * N bytes. The two loops need 2N BlockMix calls in all.

The Parameters

The RFC defines the three cost parameters and the key length like this:

  • N is the CPU and memory cost. It must be a power of 2 that is larger than 1, and less than 2^(128 * r / 8).
  • r is the block size parameter. It sets the size of each block, 128 * r bytes.
  • p is the parallelization parameter. It must be a positive integer, at most ((2^32 - 1) * 32) / (128 * r).
  • dkLen is the length of the key in bytes. It can be at most (2^32 - 1) * 32.

Memory use is 128 * r * N bytes for each running ROMix. The table sizes for r = 8 are:

N Memory
2^14 16 MiB
2^15 32 MiB
2^17 128 MiB
2^20 1024 MiB

The last row is the size of the largest test vector in the RFC, which has N = 1048576. The parameter p multiplies the time but not the memory, because the blocks can be mixed one after the other, and the same space can be reused. If the blocks are mixed at the same time, for example on several cores, each one needs its own table, and the memory is about p times larger. The OWASP Password Storage Cheat Sheet recommends Argon2id first. When that is not available, it says to use scrypt with N = 2^17 (128 MiB), r = 8, and p = 1, or one of several trade-offs, such as N = 2^16 with p = 2.

Interactive Visualizer

The visualizer above runs this exact algorithm. Enter a password and a salt as text, choose the key length, and set N, r, and p. Then click Derive key. The log starts with the first PBKDF2 call. It then shows every fill step, with the state before and after the BlockMix call, and every mix step, with the table index j and the table entry that is used. The last row shows the final PBKDF2 call and the key. To keep the log readable, the page limits N to 256, r to 8, and p to 4, and it shows the first 24 bytes of each long value. When it ends, the page tells you how many key bits change when one bit of your password is flipped.

A Worked Example

This follows the first test vector of RFC 7914. The password is empty, the salt is empty, N = 16, r = 1, p = 1, and the key has 64 bytes. To reproduce it in the visualizer, choose the 64-byte variant and clear both text fields.

Spread. PBKDF2-HMAC-SHA-256 with an empty password and an empty salt gives the 128 bytes of B:

f7ce0b653d2d72a4108cf5abe912ffdd777616dbbb27a70e8204f3ae2d0f6fad
89f68f4811d1e87bcc3bd7400a9ffd29094f0184639574f39ae5a1315217bcd7
894991447213bb226c25b54da86370fbcd984380374666bb8ffcb5bf40c254b0
67d27c51ce4ad5fed829c90b505a571b7f4d1cad6a523cda770e67bceaaf7e89

This is the input that the RFC prints for BlockMix and ROMix. The first block starts with f7ce0b653d2d72a4108cf5abe912ffdd.

The first Salsa20/8 call. BlockMix starts with X = B[1], the second block. The first call takes X xor B[0]:

7e879a214f3ec9867ca940e641718f26baee555b8c61c1b50df846116dcd3b1d
ee24f319df9b3d8514121e4b5ac5aa3276021d2909c74829edebc68db8b8c25e

The Salsa20/8 core turns it into:

a41f859c6608cc993b81cacb020cef05044b2181a2fd337dfd7b1c6396682f29
b4393168e3c9e6bcfe6bc5b7a06d96bae424cc102c91745c24ad673dc7618f81

This is the input and output pair printed in Section 8 of the RFC.

BlockMix. The second call takes this output as its starting value. With r = 1, the two outputs are already in the right order, so the BlockMix result is:

a41f859c6608cc993b81cacb020cef05044b2181a2fd337dfd7b1c6396682f29
b4393168e3c9e6bcfe6bc5b7a06d96bae424cc102c91745c24ad673dc7618f81
20edc975323881a80540f64c162dcd3c21077cfe5f8d5fe2b1a4168f953678b7
7d3b3d803b60e4ab920996e59b4d53b65d2a225877d5edf5842cb9f14eefe425

ROMix. The fill loop runs BlockMix 16 times and keeps the 16 values V[0] to V[15]. After the loop, the first 16 bytes of X are fe4e84450f63e5f4f2989ee27b3e5f58. The mix loop then reads the table in an order that comes from the data. The 16 indices j are:

4, 2, 5, 7, 9, 1, 7, 2, 13, 2, 11, 10, 10, 15, 13, 2

Index 4 comes first, so the first mix step uses V[4]. In this list, some entries are read several times, and some are never read. The result of ROMix is the output that the RFC prints in Section 10:

79ccc193629debca047f0b70604bf6b62ce3dd4a9626e355fafc6198e6ea2b46
d58413673b99b029d665c357601fb426a0b2f4bba200ee9f0a43d19b571a9c71
ef1142e65d5a266fddca832ce59faa7cac0b9cf1be2bffca300d01ee387619c4
ae12fd4438f203a0e4e1c47ec314861f4e9087cb33396a6873e8f9d2539a4b8e

In all, the example makes 32 BlockMix calls and 64 Salsa20/8 calls. The table has 2048 bytes.

Collect. The last PBKDF2 call uses the empty password and the 128 bytes above as its salt. It gives the 64-byte key that the RFC lists:

77d6576238657b203b19ca42c18a0497f16b4844e3074ae8dfdffa3fede21442
fcd0069ded0948f8326a753a0fc81f17e8d3e0fb2e0d3628cf35e20c38d18906

The other vectors of the RFC use larger parameters. The password password with the salt NaCl, N = 1024, r = 8, p = 16 gives a key that starts with fdbabe1c9d3472007856e7190d01e9fe. The password pleaseletmein with the salt SodiumChloride, N = 16384, r = 8, p = 1 gives a key that starts with 7023bdcb3afd7348461c06cd81fd38eb. The default values of the visualizer, with N = 16 and a 32-byte key, give bb4d0dfa7ff5b16333a117e947818fd23bddd65a45b1c9095c4bf0a95c19dc16. With the salt salt, changing the last letter of the password password from d to e gives a different key, which differs in 126 of the 256 bits.

Python Implementation

This is a complete scrypt. It uses the PBKDF2 function of Python’s hashlib for the two wrapper stages, and it implements Salsa20/8, BlockMix, and ROMix itself. Each function follows the RFC.

# scrypt.py
#
# scrypt, the memory-hard password-based key derivation function of Colin
# Percival (RFC 7914). It has three parts. PBKDF2-HMAC-SHA-256 turns the
# password and salt into p blocks of 128 * r bytes. Each block goes through
# ROMix, which fills a table of N blocks and then reads it back in a
# password-dependent order, so an attacker needs about N blocks of memory.
# A last PBKDF2 call turns the mixed blocks into the key.

import hashlib

MASK = 0xFFFFFFFF


def rol(x, n):
    return ((x << n) | (x >> (32 - n))) & MASK


def salsa20_8(block):
    """The Salsa20/8 core: 64 bytes in, 64 bytes out (little-endian 32-bit words)."""
    inp = [int.from_bytes(block[4 * i:4 * i + 4], "little") for i in range(16)]
    x = inp[:]
    for _ in range(4):
        # column round
        x[4] ^= rol((x[0] + x[12]) & MASK, 7)
        x[8] ^= rol((x[4] + x[0]) & MASK, 9)
        x[12] ^= rol((x[8] + x[4]) & MASK, 13)
        x[0] ^= rol((x[12] + x[8]) & MASK, 18)
        x[9] ^= rol((x[5] + x[1]) & MASK, 7)
        x[13] ^= rol((x[9] + x[5]) & MASK, 9)
        x[1] ^= rol((x[13] + x[9]) & MASK, 13)
        x[5] ^= rol((x[1] + x[13]) & MASK, 18)
        x[14] ^= rol((x[10] + x[6]) & MASK, 7)
        x[2] ^= rol((x[14] + x[10]) & MASK, 9)
        x[6] ^= rol((x[2] + x[14]) & MASK, 13)
        x[10] ^= rol((x[6] + x[2]) & MASK, 18)
        x[3] ^= rol((x[15] + x[11]) & MASK, 7)
        x[7] ^= rol((x[3] + x[15]) & MASK, 9)
        x[11] ^= rol((x[7] + x[3]) & MASK, 13)
        x[15] ^= rol((x[11] + x[7]) & MASK, 18)
        # row round
        x[1] ^= rol((x[0] + x[3]) & MASK, 7)
        x[2] ^= rol((x[1] + x[0]) & MASK, 9)
        x[3] ^= rol((x[2] + x[1]) & MASK, 13)
        x[0] ^= rol((x[3] + x[2]) & MASK, 18)
        x[6] ^= rol((x[5] + x[4]) & MASK, 7)
        x[7] ^= rol((x[6] + x[5]) & MASK, 9)
        x[4] ^= rol((x[7] + x[6]) & MASK, 13)
        x[5] ^= rol((x[4] + x[7]) & MASK, 18)
        x[11] ^= rol((x[10] + x[9]) & MASK, 7)
        x[8] ^= rol((x[11] + x[10]) & MASK, 9)
        x[9] ^= rol((x[8] + x[11]) & MASK, 13)
        x[10] ^= rol((x[9] + x[8]) & MASK, 18)
        x[12] ^= rol((x[15] + x[14]) & MASK, 7)
        x[13] ^= rol((x[12] + x[15]) & MASK, 9)
        x[14] ^= rol((x[13] + x[12]) & MASK, 13)
        x[15] ^= rol((x[14] + x[13]) & MASK, 18)
    return b"".join(((x[i] + inp[i]) & MASK).to_bytes(4, "little") for i in range(16))


def xor(a, b):
    return bytes(p ^ q for p, q in zip(a, b))


def block_mix(b, r):
    """scryptBlockMix: 2r blocks of 64 bytes in, 2r blocks out."""
    blocks = [b[64 * i:64 * i + 64] for i in range(2 * r)]
    x = blocks[-1]
    y = []
    for blk in blocks:
        x = salsa20_8(xor(x, blk))
        y.append(x)
    return b"".join(y[0::2] + y[1::2])


def integerify(x, r):
    """The last 64-byte block of x, read as a little-endian integer."""
    return int.from_bytes(x[(2 * r - 1) * 64:], "little")


def ro_mix(b, r, n):
    """scryptROMix: fill a table of n blocks, then read it back in a password-dependent order."""
    x = b
    v = []
    for _ in range(n):
        v.append(x)
        x = block_mix(x, r)
    for _ in range(n):
        j = integerify(x, r) % n
        x = block_mix(xor(x, v[j]), r)
    return x


def check_params(n, r, p, dk_len):
    if n < 2 or n & (n - 1):
        raise ValueError("N must be a power of 2 larger than 1")
    if r < 1 or p < 1 or dk_len < 1:
        raise ValueError("r, p and the key length must be positive")
    if n >= 2 ** (128 * r // 8):
        raise ValueError("N must be less than 2^(16 r)")
    if p > ((2**32 - 1) * 32) // (128 * r):
        raise ValueError("p is too large")
    if dk_len > (2**32 - 1) * 32:
        raise ValueError("the key is too long")


def scrypt(password, salt, n, r, p, dk_len):
    check_params(n, r, p, dk_len)
    b = hashlib.pbkdf2_hmac("sha256", password, salt, 1, p * 128 * r)
    mixed = b"".join(ro_mix(b[128 * r * i:128 * r * (i + 1)], r, n) for i in range(p))
    return hashlib.pbkdf2_hmac("sha256", password, mixed, 1, dk_len)


if __name__ == "__main__":
    # the first test vector of RFC 7914: empty password and salt
    key = scrypt(b"", b"", 16, 1, 1, 64)
    print("scrypt('', '', N=16, r=1, p=1):")
    print(key.hex())
    key = scrypt(b"pleaseletmein", b"SodiumChloride", 16, 1, 1, 32)
    print("scrypt('pleaseletmein', 'SodiumChloride', N=16, r=1, p=1), 32 bytes:")
    print(key.hex())

Running it produces this output:

scrypt('', '', N=16, r=1, p=1):
77d6576238657b203b19ca42c18a0497f16b4844e3074ae8dfdffa3fede21442fcd0069ded0948f8326a753a0fc81f17e8d3e0fb2e0d3628cf35e20c38d18906
scrypt('pleaseletmein', 'SodiumChloride', N=16, r=1, p=1), 32 bytes:
bb4d0dfa7ff5b16333a117e947818fd23bddd65a45b1c9095c4bf0a95c19dc16

I checked this code against several independent sources before writing it up. The Salsa20/8 core, BlockMix, and ROMix match the input and output values printed in Sections 8, 9, and 10 of RFC 7914. The code reproduces three of the four scrypt test vectors in the RFC, including N = 16384 with r = 8. The fourth uses a 1 GiB table, which is too much for a Python run, but the OpenSSL that ships with Python reproduces it. The code also agrees with that OpenSSL on 60 random parameter sets, with N from 2 to 128, r from 1 to 4, p from 1 to 3, and keys from 1 to 129 bytes. For real use, call hashlib.scrypt from the Python standard library, which this guide uses for the cross-check. The code in this guide is slow, because it runs in pure Python. The vector with N = 16384 takes about 25 seconds.

Security Status

No practical attack on scrypt is known, and its design has formal support. The RFC says that ROMix is sequential memory-hard in the random oracle model. It adds that the security of scrypt rests on an assumption. BlockMix with the Salsa20/8 core must have no shortcut that lets it be iterated more easily than a random oracle. The 2016 paper by Alwen and others proves that scrypt has optimal cumulative memory complexity in the parallel random oracle model. That model is an idealization, so it is not a proof about the real Salsa20/8 core.

Time-memory trade-offs. An attacker can keep only some of the table entries, and rebuild the others when they are needed. The program below keeps every k-th entry, for N = 1024 and r = 1, and it counts the BlockMix calls. The key is the same every time.

# scrypt_tradeoff.py
#
# The time-memory trade-off of ROMix. An attacker who does not want to keep
# all N table entries can keep every k-th entry and recompute the others
# from the closest stored one. The result is the same. The price is more
# calls of BlockMix, which is the work that scrypt wants to be expensive.

import os

import scrypt as s


def ro_mix_counting(b, r, n, k):
    """ROMix that keeps only every k-th table entry. Returns the result and the number of BlockMix calls."""
    calls = 0
    x = b
    stored = {}
    for i in range(n):
        if i % k == 0:
            stored[i] = x
        x = s.block_mix(x, r)
        calls += 1
    for _ in range(n):
        j = s.integerify(x, r) % n
        base = j - j % k
        v = stored[base]
        for _ in range(j - base):
            v = s.block_mix(v, r)
            calls += 1
        x = s.block_mix(s.xor(x, v), r)
        calls += 1
    return x, calls, len(stored)


if __name__ == "__main__":
    n, r = 1024, 1
    b = bytes(range(256))[:128 * r]
    full = s.ro_mix(b, r, n)
    print("N = %d, r = %d, table of %d bytes if every entry is kept" % (n, r, 128 * r * n))
    print("%-6s %-14s %-16s %s" % ("k", "entries kept", "BlockMix calls", "same result"))
    for k in (1, 2, 4, 8, 16, 32):
        out, calls, kept = ro_mix_counting(b, r, n, k)
        print("%-6d %-14d %-16d %s" % (k, kept, calls, out == full))
N = 1024, r = 1, table of 131072 bytes if every entry is kept
k      entries kept   BlockMix calls   same result
1      1024           2048             True
2      512            2582             True
4      256            3668             True
8      128            5724             True
16     64             9796             True
32     32             17364            True

Keeping one entry in 32 cuts the memory by a factor of 32, and the work goes up by about a factor of 8.5. The trade-off exists, but it is not free, and the cost rises with the amount of memory that the attacker saves. That is the point of the design. A bigger N raises both the memory and the work.

Data-dependent memory access. In the second loop, the index j is computed from X, which depends on the password. The pattern of memory reads therefore depends on the secret. In a setting where an attacker can watch the cache of the machine, this can leak information about the password. Argon2i avoids it by using an access pattern that does not depend on the data.

Parameters. A small N gives weak protection, whatever the algorithm. The parameters of the RFC test vectors are chosen for testing, not for use. The OWASP values quoted above are a better starting point.

FAQ

What is the difference between scrypt and PBKDF2?

PBKDF2 makes a guess slow, and scrypt makes it slow and big. PBKDF2 repeats a hash and needs almost no memory. Attackers can therefore run many copies in parallel on cheap hardware. scrypt needs a table of 128 * r * N bytes for every guess, which limits how many guesses can run at once on one chip.

What do N, r, and p do?

N sets the number of table entries, and so the memory and the time. r sets the size of each entry. A larger r makes each memory access bigger, which suits the way memory hardware works. p sets how many independent ROMix runs there are. It raises the time but not the memory.

Does scrypt need a salt?

Yes, every use of scrypt needs a salt. The salt is the second input of the first PBKDF2 call, and it makes the result different for every user, even for the same password. Use a random salt of at least 16 bytes and store it with the result.

Why does scrypt use Salsa20/8 and not a hash function?

The mixing function does not have to be collision resistant, only hard to shortcut. The core is fast and simple, and its 8 rounds are a reduced version of the 20 rounds of Salsa20. The RFC states that the core is not a cryptographic hash function.

Should I use scrypt or Argon2id?

OWASP recommends Argon2id as the first choice, and scrypt when Argon2id is not available. The two are close in strength when scrypt has good parameters. See the Argon2 guide for the other design.

References

  1. Percival, C. and Josefsson, S. “The scrypt Password-Based Key Derivation Function.” RFC 7914, 2016. Available at: https://www.rfc-editor.org/rfc/rfc7914

  2. Percival, C. “Stronger Key Derivation via Sequential Memory-Hard Functions.” BSDCan 2009. Available at: https://www.tarsnap.com/scrypt/scrypt.pdf

  3. Alwen, J., Chen, B., Pietrzak, K., Reyzin, L., and Tessaro, S. “Scrypt is Maximally Memory-Hard.” Cryptology ePrint Archive, Paper 2016/989. Available at: https://eprint.iacr.org/2016/989

  4. Bernstein, Daniel J. “Salsa20 specification.” 2005. Available at: https://cr.yp.to/snuffle/spec.pdf

  5. OWASP. “Password Storage Cheat Sheet.” Available at: https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html

Key Derivation Functions Advanced

The Balloon Hashing Algorithm

Balloon hashing is a memory-hard password hashing function built from an ordinary hash such as SHA-256. Learn how it expands a buffer, mixes it with salt-driven reads, why its memory access never depends on the password, and what attacks it faces.

16 min read

Key Derivation Functions Intermediate

Argon2: Memory-Hard Password Hashing

Argon2 won the 2015 Password Hashing Competition by making brute-force attacks expensive in RAM, not just CPU time. That closes off the GPU and ASIC cracking farms that broke older schemes. Here's how memory-hardness works.

9 min read

Key Derivation Functions Advanced

The bcrypt Algorithm

bcrypt is a password hashing function built from the Blowfish cipher with a deliberately expensive key setup. Learn how the salt, the cost, and the 72-byte limit work, how the hash string is built, and why its versions differ.

20 min read