PBKDF2 ships with the JDK, so why pull in a library for bcrypt, scrypt, or Argon2, and how do you use them on the JVM?
answer
- PBKDF2 = CPU-hard only; weak vs GPU/ASIC
- bcrypt/scrypt/Argon2 add memory-hardness
- Argon2id = PHC winner, modern default
- JDK ships only PBKDF2 -> need a library
- Spring PasswordEncoder + DelegatingPasswordEncoder for migration
basics
~20 sPBKDF2 only stresses CPU, so it's cheap to crack on GPUs/ASICs. bcrypt, scrypt, and Argon2 are also memory-hard, raising attacker cost much more. The JDK has no built-in for them, so you add a library like Spring Security's PasswordEncoder, jBCrypt, or Bouncy Castle.
solid answer
~40 sPBKDF2 is the only password KDF bundled in the JCA, and it is fine when configured well, but it is purely CPU-bound: attackers with GPUs or ASICs parallelize it cheaply. bcrypt adds modest memory use and a built-in salt; scrypt and especially Argon2 (the 2015 Password Hashing Competition winner) are deliberately memory-hard, so each guess needs significant RAM, which is expensive to scale on specialized hardware. The JDK ships none of these, so you bind a library. The cleanest JVM path is Spring Security's PasswordEncoder abstraction with BCryptPasswordEncoder, SCryptPasswordEncoder, Argon2PasswordEncoder, or Pbkdf2PasswordEncoder, plus DelegatingPasswordEncoder, which prefixes each hash with its scheme id ({argon2}...) so you can run multiple algorithms and migrate. Under the hood those wrap jBCrypt or Bouncy Castle. Argon2id is the current default recommendation for new systems.
code
java · 14 lines// Spring Security: one encoder that delegates by scheme prefix
import org.springframework.security.crypto.factory.PasswordEncoderFactories;
import org.springframework.security.crypto.password.PasswordEncoder;
PasswordEncoder encoder = PasswordEncoderFactories.createDelegatingPasswordEncoder();
String stored = encoder.encode("s3cret"); // -> {argon2}$argon2id$v=19$m=...,t=...,p=...$salt$hash
boolean valid = encoder.matches("s3cret", stored); // constant-time verify, salt read from the string
// migrate weak hashes on successful login
if (valid && encoder.upgradeEncoding(stored)) {
String upgraded = encoder.encode("s3cret");
// persist `upgraded`
}go deeper
Knows the names bcrypt/scrypt/Argon2 exist as stronger alternatives and that PBKDF2 is the built-in one.
Can use a library (e.g. BCryptPasswordEncoder) via encode/matches and knows PBKDF2 is CPU-only.
Explains memory-hardness, picks Argon2id, uses DelegatingPasswordEncoder for migration, and knows bcrypt's 72-byte limit and Argon2 variants.
Sets organizational KDF policy, weighs FIPS/compliance vs Argon2, sizes memory/time parameters against capacity, and plans cross-service migration off legacy hashes.
## Why PBKDF2 alone is not the end of the story PBKDF2 makes guessing slow by repeating a hash many times - it burns **CPU time**. That's good, but it has a weakness: it needs almost **no memory**. Attackers don't crack passwords on normal CPUs; they use **GPUs** (thousands of parallel cores) and custom **ASICs**/FPGAs. Because PBKDF2 is tiny in memory, you can run tens of thousands of PBKDF2 computations in parallel on one cheap card. So PBKDF2 raises cost less than you'd hope against a serious attacker. ## Memory-hardness: the next lever The fix is to make each guess require a lot of **RAM**, not just CPU. RAM is expensive and hard to parallelize cheaply on a GPU/ASIC, so a **memory-hard** KDF narrows the gap between your server and the attacker's hardware. The main families: - **bcrypt** (1999) - based on the Blowfish key schedule, has a built-in salt and a `cost`/work factor. It uses a fixed, modest amount of memory (4 KB), so it's *somewhat* GPU-resistant but not strongly memory-hard. It also silently truncates passwords beyond 72 bytes - a real gotcha. Still a solid, battle-tested choice. - **scrypt** (2009) - explicitly memory-hard with tunable parameters `N` (CPU/memory cost), `r` (block size), `p` (parallelism). Much harder to attack on GPUs than PBKDF2/bcrypt. - **Argon2** (2015) - winner of the **Password Hashing Competition**, the modern recommendation. Tunable **memory**, **iterations (time)**, and **parallelism**. Three variants: **Argon2d** (max GPU resistance, but data-dependent memory access -> side-channel risk), **Argon2i** (side-channel resistant), and **Argon2id** (a hybrid - the recommended default). ## Why a library at all The **JDK/JCA only ships PBKDF2** (`SecretKeyFactory` with `PBKDF2WithHmac*`). There is no built-in bcrypt, scrypt, or Argon2. To use those you add a dependency. Common JVM options: - **Spring Security crypto** - the most ergonomic. It defines a `PasswordEncoder` interface with `encode(rawPassword)` and `matches(raw, encoded)`. Implementations: `BCryptPasswordEncoder`, `SCryptPasswordEncoder`, `Argon2PasswordEncoder`, `Pbkdf2PasswordEncoder`. - **`DelegatingPasswordEncoder`** - wraps several encoders and writes a scheme prefix into every hash, like `{argon2}$argon2id$v=19$m=...` or `{bcrypt}$2a$10$...`. On `matches`, it reads the prefix and dispatches to the right encoder. This is what makes **algorithm migration** painless: set your default to `{argon2}`, keep the old encoders registered to verify legacy hashes, and rehash on successful login. - **jBCrypt** / **Bouncy Castle** - lower-level libraries that Spring (and others) build on; Bouncy Castle provides Argon2 and scrypt primitives directly. ## How you actually call it (Spring Security example) ``` PasswordEncoder enc = PasswordEncoderFactories.createDelegatingPasswordEncoder(); String stored = enc.encode(rawPassword); // {argon2}$argon2id$... boolean ok = enc.matches(rawPassword, stored); if (ok && enc.upgradeEncoding(stored)) { /* rehash with stronger params */ } ``` `encode` salts and hashes (the salt is embedded in the output string), `matches` verifies in constant time, and `upgradeEncoding` tells you when a stored hash uses outdated parameters so you can rehash. ## Choosing - New system: **Argon2id**, sized so one hash takes ~tens of ms with a meaningful memory cost (e.g. tens of MB). - Can't add Argon2 (e.g. **FIPS** environments often mandate PBKDF2): use **PBKDF2WithHmacSHA256/512** with a high iteration count - it's the only NIST-approved one of the group. - Legacy/widely-portable: bcrypt is fine, mind the 72-byte truncation. The key interview point: PBKDF2 is *adequate* and built-in; memory-hard KDFs are *better* against modern cracking hardware, and you reach them through a library, ideally behind `PasswordEncoder` + `DelegatingPasswordEncoder` so you can migrate.
- What does Spring Security's DelegatingPasswordEncoder do and why is it useful?It stores a scheme prefix (e.g. {argon2}, {bcrypt}) in every hash and dispatches verification to the matching encoder. That lets you support multiple algorithms at once, set a strong default, still verify legacy hashes, and migrate users by rehashing on login - all without a flag-day reset.
- When might you be forced to stick with PBKDF2 over Argon2?In FIPS 140-validated / NIST-compliant environments: PBKDF2 is the NIST-approved password KDF, while Argon2 and scrypt are not (yet) FIPS-approved. There you use PBKDF2WithHmacSHA256/512 with a high iteration count and a validated crypto module.
saying these in an interview costs you the question
- Claiming the JDK has built-in bcrypt/Argon2 (it does not)
- Saying PBKDF2 is memory-hard (it isn't)
- Recommending Argon2d for general use (use Argon2id; 2d risks side-channels)
- Ignoring bcrypt's 72-byte password truncation
- Rolling your own KDF instead of a vetted library