Back to the journalNOTES BY FAJAR
Cryptography4 min read

[Crypto: age] scrypt, From Password to Key

Learn how scrypt derives an age wrapping key, why each attempt uses time and memory, and what a slow calculation cannot prevent.

PART 3 OF 6Crypto: age
  1. 01[Crypto: age] From Password to Ciphertext
  2. 02[Crypto: age] Reading the Header
  3. 03[Crypto: age] scrypt, From Password to KeyYou are here
  4. 04[Crypto: age] ChaCha20-Poly1305, Sealing the Body
  5. 05[Crypto: age] Passphrase or Key Pair
  6. 06[Crypto: age] How Long Does a Passphrase Attack Take?
In this article 5 sections

age uses scrypt to derive a key from a passphrase. A key derivation function is an algorithm that calculates a key from other input values. Each scrypt operation uses time and memory.

The header post showed a salt and a work factor. Together with the passphrase, those values let scrypt make the same wrapping key during encryption and decryption.

A longer key does not make a passphrase less predictable

The encryption algorithm uses a key of a fixed size. A passphrase can have a different length. scrypt converts the passphrase into a key of that size.

This conversion does not add random choices to the passphrase. If an attacker tries the correct passphrase, scrypt makes the correct key. A common password remains predictable even when the derived key has 32 bytes.

The useful cost is the work for each attempt. The owner usually derives the key once to decrypt a file. An attacker must repeat the calculation for each candidate passphrase.

The parameters in an age file

For the example header, the stored work factor is 18. age interprets this as N = 2^18, or 262,144.

The pseudocode shows the inputs. It is a formula, not a command:

text
wrapping key = scrypt(
    passphrase = user input,
    salt = "age-encryption.org/v1/scrypt" followed by the 16 salt bytes,
    N = 262144,
    r = 8,
    p = 1,
    output length = 32 bytes
)
ParameterMeaning in this example
NThe primary cost parameter
rA block size parameter, fixed at 8
pA parallelization parameter, fixed at 1
SaltA fixed age label followed by the random salt from the header
Output length32 bytes for the wrapping key

The fixed label separates this use of scrypt from uses with different labels. The random salt also makes the calculation specific to this file. Neither value is a secret.

The age 1.3.1 implementation uses these parameters and defaults to work factor 18. The format permits other work factors. Thus, the header value matters when age decrypts a file.

Why one attempt uses about 256 MiB

scrypt uses a large memory area during its calculation. The primary memory area has approximately 128 × N × r bytes.

For this example:

text
128 × 262144 × 8 = 268435456 bytes = 256 MiB

MiB means mebibytes. One MiB is 1,048,576 bytes. GiB means gibibytes. One GiB is 1,024 MiB. The program also uses some memory outside this primary area.

Ten attempts at the same time use about 2.5 GiB for their primary memory areas. This requirement makes large parallel attacks more expensive. Memory capacity limits how many attempts fit at once. Memory bandwidth limits how quickly the hardware can transfer data.

These limits do not prevent an attack. Hardware and implementation choices affect the rate. Some implementations exchange more calculation work for less memory. The scrypt specification gives the algorithm and its parameters.

Measure scrypt on your computer

The initial measurement used an Apple M5 and Python hashlib.scrypt. It recorded about 0.3701 seconds for each calculation, or 2.70 calculations each second. Those figures apply to that measurement. Other computers can give different results.

The Python example measures only scrypt. It does not decrypt an age file or test the authentication tag. Its salt includes the age label used in the file format.

Use Python 3 with hashlib.scrypt support. Make sure that more than 256 MiB of memory is available. The Python documentation gives the function parameters.

  1. Save this code in a new file called measure_scrypt.py:

    python
    import hashlib
    import os
    import time
    
    cost = 1 << 18
    block_size = 8
    parallelism = 1
    main_memory = 128 * cost * block_size
    salt = b"age-encryption.org/v1/scrypt" + os.urandom(16)
    
    def derive_key(passphrase):
        return hashlib.scrypt(
            passphrase,
            salt=salt,
            n=cost,
            r=block_size,
            p=parallelism,
            dklen=32,
            # Permit memory for internal work outside the main area.
            maxmem=main_memory + (64 << 20),
        )
    
    # Exclude the initial call from the measured calls.
    derive_key(b"warmup")
    attempts = 8
    start = time.perf_counter()
    for index in range(attempts):
        derive_key(f"guess{index}".encode("ascii"))
    elapsed = time.perf_counter() - start
    
    print("seconds per calculation:", round(elapsed / attempts, 4))
    print("calculations per second:", round(attempts / elapsed, 2))
    
  2. Run the file:

    bash
    python3 measure_scrypt.py
    

The first output number is the average time for one calculation. The second number is the number of calculations each second. Your results can differ with processor speed, memory, Python version, and other running programs.

What a higher work factor changes

An increase from 18 to 19 doubles N. The primary memory area increases from 256 MiB to 512 MiB. At work factor 20, it increases to 1 GiB.

The time usually increases too, but you must measure it for an accurate result. A larger work factor also increases the cost of legitimate decryption.

A slow calculation cannot compensate for a predictable passphrase. If the correct passphrase is the first candidate, only one attempt is necessary. Random selection from a large set increases the expected number of attempts in an exhaustive search.

The attack time post calculates this difference. First, the next post follows the wrapping key to the encrypted file key and payload.

FILED UNDER

NEXT IN THIS SERIES[Crypto: age] ChaCha20-Poly1305, Sealing the Body

THANKS FOR READING

Did this resonate?

A reaction or a conversation is always welcome.

Loading reactions…

Pass it along

Loading comments...

KEEP EXPLORING

One thought leads to another.

All writing
Back to all writingOne note at a time.