AES Cipher Β· 6 min read
PBKDF2, Argon2, and bcrypt: The Password-to-Key Derivation Problem
Why you can't use a password directly as an AES key, how PBKDF2, bcrypt, and Argon2 stretch passwords into keys, and which to pick in 2026.
AES-256 wants exactly 32 bytes of cryptographically-strong key material. A human password β even a long one β is neither 32 bytes nor cryptographically strong. The bridge between the two is a key derivation function: a deliberately slow, deliberately expensive transformation that turns a guessable password into something AES can use. The three names you'll see are PBKDF2, bcrypt, and Argon2. They solve the same problem with different tradeoffs.
Why Not Just Hash the Password?
The naive answer is SHA256(password). It produces 32 bytes, which is the right shape. But SHA-256 is fast β a modern GPU can compute billions per second. An attacker who steals an encrypted file and wants to brute-force the password can try every dictionary word, every common phrase, every leaked-database password in minutes. The hash function isn't the bottleneck; it's the candidate list, and the candidate list is small.
The fix is to make each guess expensive. If a single password attempt takes 100 milliseconds instead of one nanosecond, the same dictionary attack now takes weeks. That's the whole game.
PBKDF2: The Old Standard
PBKDF2 (RFC 2898, 2000) is just HMAC iterated many times. You feed it a password, a salt, an iteration count, and a desired output length; it runs HMAC-SHA256 (or SHA-512) that many times in a chain. OWASP's 2024 guidance recommends 600,000 iterations of HMAC-SHA256 for new deployments.
PBKDF2's strength is ubiquity β it's in every standard library, every TLS stack, every password manager. Its weakness is that it's purely CPU-bound. GPUs and ASICs can parallelise PBKDF2 cheaply, so the cost asymmetry between attacker and defender is smaller than it looks. Use PBKDF2 when you have to (FIPS compliance, legacy interop), but don't pick it for greenfield code in 2026.
bcrypt: Memory-Lite, Battle-Tested
bcrypt (Provos and MaziΓ¨res, 1999) is built around a modified Blowfish key schedule that's deliberately awkward to parallelise. It uses about 4 KB of working memory per hash, which is enough to take GPUs out of their comfort zone β GPU hardware shines on small, parallel, identical workloads, and bcrypt's memory access pattern doesn't fit.
bcrypt has a fixed 72-byte input limit (anything longer is silently truncated) and a fixed 184-bit output. That's fine for password verification but awkward when you want a 256-bit AES key. In practice you bcrypt the password, then run the result through HKDF or a simple SHA-256 to expand to the size you need. bcrypt has been the default for password storage in Ruby on Rails, Django (until recently), and PHP for over a decade.
Argon2: The Modern Default
Argon2 won the Password Hashing Competition in 2015 and was standardised as RFC 9106 in 2021. It's memory-hard by design: you tune three parameters β memory cost, time cost, and parallelism β and the algorithm forces the attacker to allocate that much RAM per guess. With memory set to 64 MB, a single GPU with 24 GB of VRAM can only run ~375 attempts in parallel instead of millions.
There are three variants. Argon2d is fastest but susceptible to side-channel attacks. Argon2i is side-channel resistant but slower. Argon2id is a hybrid and the recommended default β it gets the side-channel resistance of Argon2i for the first half of the computation and the raw speed of Argon2d for the second half. OWASP's 2024 baseline is Argon2id with 19 MiB memory, 2 iterations, and 1 degree of parallelism, scaled up if your hardware allows.
Salts and Why They're Not Optional
Every KDF takes a salt β a unique, random, per-password value. Without a salt, an attacker can precompute a rainbow table of common-password-to-key mappings once and reuse it against every user in every breach. With a per-password salt, each guess has to be recomputed for each target. Salts don't need to be secret; they just need to be unique. 16 random bytes is the standard size.
What to Pick
- New code, no constraints: Argon2id. RFC-standardised, memory-hard, future-proof.
- FIPS-regulated environment: PBKDF2-HMAC-SHA256 with 600,000+ iterations.
- Existing bcrypt deployment: Stay on bcrypt with cost factor 12 or higher. It's still secure; migrating is rarely worth the operational cost.
- scrypt: Memory-hard like Argon2 but pre-dates the Password Hashing Competition. Fine for existing deployments; pick Argon2 for new ones.
The Quiet Rule
Whichever KDF you pick, store the parameters alongside the output. The standard format is $argon2id$v=19$m=65536,t=3,p=1$salt$hashβ or the equivalent encoding for bcrypt and PBKDF2. Encoding the parameters lets you increase the cost over time without breaking existing users: when someone logs in successfully, check the cost; if it's below your current target, rehash and update the database. A KDF you can't crank up is a KDF that's permanently stuck at 2026 cost levels in 2036.
References
- Kaliski, B. (2000). RFC 2898: PKCS #5: Password-Based Cryptography Specification Version 2.0. Internet Engineering Task Force.
- Provos, N. & Mazières, D. (1999). A Future-Adaptable Password Scheme. USENIX Annual Technical Conference.
- Biryukov, A., Dinu, D., & Khovratovich, D. (2016). Argon2: the memory-hard function for password hashing and other applications. Password Hashing Competition.
- Biryukov, A., Dinu, D., Khovratovich, D., & Josefsson, S. (2021). RFC 9106: Argon2 Memory-Hard Function for Password Hashing and Proof-of-Work Applications. Internet Engineering Task Force.
- OWASP. (2024). Password Storage Cheat Sheet. Open Web Application Security Project.