Key Derivation Functions
Most keys do not start as random bytes; they start as human secrets - passwords, passphrases, or short phrases. A key derivation function (KDF) stretches such a secret into a fixed-length key suitable for an encryption algorithm, using salt plus a work factor so that brute force stays expensive. The three standard KDFs are PBKDF2, scrypt, and Argon2, and all of them follow the same interface: (secret, salt, work) -> key.
PBKDF2 (hashlib and cryptography)
import hashlib
import secrets
password = b"correct horse battery staple"
salt = secrets.token_bytes(16)
# standard library
key = hashlib.pbkdf2_hmac("sha256", password, salt, 600_000, dklen=32)
print(key.hex())
# cryptography library
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
kdf = PBKDF2HMAC(
algorithm=hashes.SHA256(),
length=32,
salt=salt,
iterations=600_000,
)
key = kdf.derive(password) # 32 bytes = AES-256 key
PBKDF2 applies an HMAC iteratively; the iteration count is the cost knob. It is CPU-bound, which means GPUs accelerate it well - for that reason memory-hard functions exist.
scrypt
scrypt adds a memory requirement on top of the CPU work, so each guessing thread needs its own memory block. The cryptography library exposes it:
from cryptography.hazmat.primitives.kdf.scrypt import Scrypt
kdf = Scrypt(salt=salt, length=32, n=2**14, r=8, p=1)
key = kdf.derive(password) # 16 MiB memory per guess at these parameters
The parameters are n (CPU/memory cost, a power of two), r (block size), and p (parallelization). Doubling n doubles both time and memory per guess, which is the point when defending against dedicated hardware.
Argon2
import argon2
# Argon2id with explicit parameters (for key derivation, not just passwords)
key = argon2.low_level.hash_secret_raw(
secret=password,
salt=salt,
time_cost=3, # iterations
memory_cost=65536, # 64 MiB
parallelism=4,
hash_len=32,
type=argon2.low_level.Type.ID,
)
print(len(key)) # 32
Choosing Parameters
- Tune the work factor so one derivation takes about 100-500 ms on your target hardware, and re-tune periodically.
- Store the salt and all parameters next to the derived key (or in the encoded string, as Argon2 does) - nothing about them is secret.
- Derive different keys for different purposes by including a context/domain string, for example
pbkdf2_hmac(..., b"app:db-key"), so the database key can never double as the API key.
Summary
Turn weak secrets into strong keys with a salted KDF: PBKDF2 for portability, scrypt or Argon2 when attackers have hardware to spare. Bake the work factor and the purpose into the stored record, and never derive two different keys from the same salt without a domain separator.