Introduction
PBKDF2 was standardised by RSA Laboratories as RFC 2898 in September 2000; bcrypt was published by Niels Provos and David Mazieres at USENIX Security 1999; scrypt was published by Colin Percival at USENIX Security 2009; Argon2 was published by Alex Biryukov, Daniel Dinu, and Dmitry Khovratovich and won the Password Hashing Competition in July 2015, then was standardised as RFC 9106 in September 2021. The key derivation comparison java topic matters because every Java backend chooses one of these four KDFs for password storage, and the choice is driven by a trade-off between CPU cost, memory cost, and standard library availability that this tutorial benchmarks directly.
For a Java learner, the key derivation comparison java topic is the practical entry point into the modern password-hash design space. The JDK ships PBKDF2 via SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256") but does not ship scrypt, bcrypt, or Argon2 in the standard library; those require Bouncy Castle on the classpath. This tutorial benchmarks PBKDF2 in pure JDK at 1,000, 10,000, 100,000, and 600,000 iterations to show the iteration-count-to-wall-time trade-off, and prints the Bouncy Castle classpath instructions and API entry points needed to add scrypt, bcrypt, and Argon2 to the same harness.
What is Key Derivation Comparison?
Key derivation comparison is the side-by-side evaluation of password-based key derivation functions (PBKDF2, scrypt, bcrypt, Argon2) on four axes: CPU cost (how many milliseconds a single hash takes), memory cost (how many megabytes of RAM a single hash uses), standard library availability (whether the JDK ships the algorithm or whether an external library is required), and cryptanalytic track record (how many years the algorithm has been studied without a published break). The right KDF for a given application balances all four axes against the application's login-latency budget and threat model.
PBKDF2 belongs to the family of CPU-hard KDFs: it iterates HMAC a configurable number of times, with no memory cost beyond a few hundred bytes of HMAC state. bcrypt belongs to the family of CPU-and-EPOP-hard KDFs: it uses a 4 KB Blowfish key schedule that is expensive to compute but small enough to fit in L1 cache, which is why bcrypt is GPU-friendly and brute-forceable at scale. scrypt and Argon2 belong to the family of memory-hard KDFs: they allocate and overwrite large memory regions (scrypt uses N * r * 128 bytes, Argon2 uses m KiB) so that an attacker cannot parallelise the brute force on cheap GPUs without paying for the equivalent memory.
The distinguishing trait of memory-hard KDFs is that they raise the attacker's cost per password by a factor equal to the memory cost divided by the available GPU per-core memory, which is typically 64-256 KB on consumer GPUs. A 19 MiB Argon2id hash forces the attacker to use 64-128x more silicon than a 0.5 MiB PBKDF2 hash, which is the reason OWASP 2023 lists Argon2id as the primary recommendation and PBKDF2 as the fallback.
How Key Derivation Comparison Works - Step by Step
Step 1: Define a benchmark harness. The harness takes a KDF name, a parameter set, a sample password, a sample salt, and a measurement function (wall clock via System.nanoTime). The harness runs the KDF once to warm up the JIT, then runs it again and reports the elapsed time in milliseconds.
Step 2: PBKDF2-HMAC-SHA-256 with 1,000 iterations. The fastest variant; SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256") with a PBEKeySpec of 1,000 iterations and 256-bit output. Wall time: roughly 0.4 milliseconds on a modern x86-64 CPU. This is far below the OWASP minimum and is included for comparison only.
Step 3: PBKDF2-HMAC-SHA-256 with 10,000 iterations. The 2015 OWASP minimum; wall time roughly 4 milliseconds. Still below the 2023 minimum, included to show the linear scaling of PBKDF2 cost with iteration count.
Step 4: PBKDF2-HMAC-SHA-256 with 100,000 iterations. The 2018 OWASP minimum; wall time roughly 40 milliseconds. Half the OWASP 2023 minimum, included to anchor the 2023 minimum.
Step 5: PBKDF2-HMAC-SHA-256 with 600,000 iterations. The OWASP 2023 minimum; wall time roughly 250 milliseconds. The right baseline for any new PBKDF2 deployment in 2025.
Step 6: For scrypt, bcrypt, and Argon2, add Bouncy Castle to the classpath: javac -cp bcprov-jdk18on.jar KdfComparisonDemo.java and java -cp bcprov-jdk18on.jar:. KdfComparisonDemo. Use org.bouncycastle.crypto.generators.SCrypt.generate(pw, salt, N=32768, r=8, p=1, dkLen=32) for scrypt, org.bouncycastle.crypto.generators.BCrypt.hashpw(pw, BCrypt.gensalt(cost=12)) for bcrypt, and Argon2BytesGenerator with Argon2Parameters (m=19456, t=2, p=1) for Argon2id. Wall times: roughly 100 ms for scrypt N=32768, 250 ms for bcrypt cost=12, and 200 ms for Argon2id m=19 MiB t=2.
Step 7: Report the results side by side. The comparison shows that all four KDFs reach the 200-250 ms wall-time target that is appropriate for interactive login flows, but the memory-hard KDFs (scrypt and Argon2id) impose 16-19 MiB of memory per hash that makes GPU brute force uneconomical, while PBKDF2 and bcrypt are GPU-friendly and require higher iteration counts to match the same attacker cost.
Java Program to Compare PBKDF2 scrypt bcrypt Argon2 Performance
import javax.crypto.SecretKey;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.PBEKeySpec;
import java.security.SecureRandom;
public class KdfComparisonDemo {
static byte[] randomSalt(int n) {
byte[] s = new byte[n];
new SecureRandom().nextBytes(s);
return s;
}
static String toHex(byte[] b) {
StringBuilder sb = new StringBuilder(b.length * 2);
for (byte x : b) {
sb.append(Character.forDigit((x >> 4) & 0xf, 16));
sb.append(Character.forDigit(x & 0xf, 16));
}
return sb.toString();
}
static long measureMs(Runnable op) {
long start = System.nanoTime();
op.run();
long end = System.nanoTime();
return (end - start) / 1_000_000;
}
static byte[] pbkdf2(char[] pw, byte[] salt, int iters, int outBits) throws Exception {
SecretKeyFactory skf = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256");
PBEKeySpec spec = new PBEKeySpec(pw, salt, iters, outBits);
SecretKey k = skf.generateSecret(spec);
spec.clearPassword();
return k.getEncoded();
}
public static void main(String[] args) throws Exception {
char[] password = "hunter2".toCharArray();
byte[] salt = randomSalt(16);
System.out.println("Salt (16 B, hex): " + toHex(salt));
System.out.println();
// Warm up the JIT by running a single short PBKDF2 derivation first.
pbkdf2(password, salt, 1_000, 256);
System.out.println("PBKDF2-HMAC-SHA-256 wall time at progressive iteration counts:");
int[] iterCounts = {1_000, 10_000, 100_000, 600_000};
for (int iters : iterCounts) {
final int i = iters;
long t = measureMs(() -> {
try {
byte[] h = pbkdf2(password, salt, i, 256);
System.out.println(" PBKDF2 " + i + " iters: " + toHex(h).substring(0, 16) + "...");
} catch (Exception e) {
throw new RuntimeException(e);
}
});
System.out.printf(" PBKDF2 iters=%7d elapsed=%5d ms%n", iters, t);
}
System.out.println();
// scrypt, bcrypt, Argon2 require Bouncy Castle on the classpath:
// javac -cp bcprov-jdk18on.jar KdfComparisonDemo.java
// java -cp bcprov-jdk18on.jar:. KdfComparisonDemo
System.out.println("Bouncy Castle (bcprov-jdk18on.jar) API summary:");
System.out.println(" SCrypt.generate(pw, salt, N=32768, r=8, p=1, dkLen=32)");
System.out.println(" BCrypt.hashpw(pw, BCrypt.gensalt(cost=12))");
System.out.println(" Argon2BytesGenerator + Argon2Parameters (m=19456, t=2, p=1)");
System.out.println();
System.out.println("Expected wall times on a modern x86-64 CPU:");
System.out.println(" PBKDF2 600,000 iters ~250 ms (CPU-only, GPU-friendly)");
System.out.println(" bcrypt cost=12 ~250 ms (4 KB memory, GPU-friendly)");
System.out.println(" scrypt N=32768,r=8 ~100 ms (32 MiB memory, GPU-resistant)");
System.out.println(" Argon2 m=19456,t=2,p=1 ~200 ms (19 MiB memory, GPU-resistant)");
}
}
Code Explanation
The randomSalt helper wraps SecureRandom.nextBytes for the salt generation. The salt is 16 bytes (128 bits), which is the OWASP minimum and gives the salt enough entropy that two users with the same password never collide on the same salt value with probability below 2^-128.
The toHex helper converts a byte array to its lowercase hex representation; the demo prints only the first 16 hex characters (8 bytes) of each derived key for brevity. The full 32-byte derived key would be 64 hex characters.
The measureMs helper takes a Runnable, records the start time in nanoseconds via System.nanoTime, runs the runnable, records the end time, and returns the difference in milliseconds. System.nanoTime is the right primitive for measuring elapsed time because it is monotonic (not subject to wall-clock adjustments) and has nanosecond resolution on modern JVMs.
The pbkdf2 method wraps SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256") with a PBEKeySpec carrying the password, salt, iteration count, and output length in bits. The 256-bit output is the right length for an AES-256 key or for a password hash; spec.clearPassword() zeroes the password char array inside the spec immediately after use, which is a small defence against heap dumps. The method returns k.getEncoded() as the derived key bytes.
The main method first warms up the JIT with a single short PBKDF2 run, then iterates over the four iteration counts (1,000, 10,000, 100,000, 600,000) and times each one. The warm-up is important because the first invocation of SecretKeyFactory.getInstance and the first HMAC-SHA-256 block computation are not yet JIT-compiled, so the first measured time would be inflated by class loading and interpreter overhead. After the warm-up, all four measurements reflect the steady-state CPU cost.
The lambda () -> { ... } passed to measureMs wraps the PBKDF2 call and its print, catching the checked Exception and rethrowing it as a RuntimeException so the lambda can satisfy the Runnable interface. The final int i = iters line captures the loop variable for the lambda, which requires the captured variable to be effectively final.
The final block of main prints the Bouncy Castle classpath instructions and API entry points for scrypt, bcrypt, and Argon2, plus the expected wall times on a modern x86-64 CPU. These are not measured by the demo because the demo runs in pure JDK; they are the published wall times for the OWASP-recommended parameter sets, calibrated against the same kind of CPU the PBKDF2 measurements are taken on.
Interview Questions and Answers
Q1. What is the difference between CPU-hard and memory-hard KDFs?
CPU-hard KDFs (PBKDF2, bcrypt) make brute force expensive by iterating a CPU-bound operation many times, but they use only a few hundred bytes (PBKDF2) or 4 KB (bcrypt) of memory per hash, which fits easily in GPU per-core memory and lets an attacker parallelise the brute force on cheap consumer GPUs. Memory-hard KDFs (scrypt, Argon2) allocate and overwrite large memory regions (16-32 MiB per hash) so that the attacker cannot parallelise without paying for the equivalent memory on each parallel core, which raises the attacker's cost by 64-128x compared to CPU-hard KDFs.
Q2. Why does PBKDF2 scale linearly with the iteration count while bcrypt scales exponentially with the cost factor?
PBKDF2 iterates HMAC a fixed number of times, so doubling the iteration count doubles the wall time (linear scaling). bcrypt's cost factor is the base-2 logarithm of the iteration count, so incrementing the cost factor by 1 doubles the iteration count (exponential scaling in cost). A PBKDF2 of 600,000 iterations and a bcrypt of cost 12 (2^12 = 4,096 iterations) both take roughly 250 milliseconds on a modern CPU; the bcrypt's exponential scaling means raising the cost to 13 (8,192 iterations) takes 500 ms, while PBKDF2 at 1.2 million takes the same 500 ms.
Q3. Why is Argon2id preferred over scrypt and bcrypt in 2025?
Argon2id won the 2015 Password Hashing Competition and was standardised as RFC 9106 in 2021, giving it the most recent cryptanalytic review of the four KDFs. It is a hybrid of Argon2i (data-independent memory access, side-channel resistant) and Argon2d (data-dependent memory access, tradeoff-resistant): the first half of memory is data-independent to defeat cache-timing attacks, the second half is data-dependent to defeat tradeoff attacks. OWASP 2023 lists Argon2id as the primary recommendation and the others as fallbacks for legacy compatibility.
Q4. What's the impact of running PBKDF2 with 1,000 iterations in 2025?
PBKDF2 with 1,000 iterations is the RFC 2898 default from 2000 and is six hundred times below the OWASP 2023 minimum. A single hash takes 0.4 milliseconds on a modern CPU, which means a stolen database can be brute-forced at roughly 2,500 guesses per second per CPU core, or roughly 100,000 guesses per second on a modern GPU. Every password under 12 characters in the stolen database will be recovered in hours. Never use 1,000 iterations for new deployments; the OWASP 2023 minimum is 600,000 for PBKDF2-HMAC-SHA-256.
Q5. Why does the JDK not ship scrypt, bcrypt, or Argon2 in the standard library?
The JDK ships only PBKDF2 because it was standardised as a Java standard request (JSR 287) and integrated into the javax.crypto package via SecretKeyFactory in JDK 8. scrypt, bcrypt, and Argon2 have not been standardised by the JCP and would require a new JSR or a JEP to add to the standard library; the OpenJDK community tracks Argon2 via JEP draft 8341759 but has not shipped it in JDK 23. The Bouncy Castle library (bcprov-jdk18on 1.78+) provides all three with the OWASP-recommended parameters and is the standard production choice.
Q6. How would you migrate a legacy PBKDF2 user database to Argon2id?
Use the lazy-migration pattern: store the algorithm and parameters in the encoded record (e.g., pbkdf2$600000$salt$hash), and on every login check the algorithm field. If it is PBKDF2, verify the password against the stored PBKDF2 hash, and if it succeeds, immediately re-hash with Argon2id and update the record. This upgrades every user to Argon2id the next time they log in without forcing a password reset; users who never log in again are never upgraded, but their PBKDF2 hashes remain valid until the next login. Spring Security's DelegatingPasswordEncoder implements this pattern out of the box.
Q7. A team plans to switch from bcrypt cost=10 to Argon2id m=19 MiB. What's the operational impact?
The wall time per login roughly doubles (from 100 ms to 200 ms), and the per-login memory usage jumps from 4 KB to 19 MiB. At 1,000 logins per second the bcrypt server uses 4 MB of memory and 1 CPU-second per second; the Argon2id server uses 19 GB of memory and 200 CPU-seconds per second, which is a 5,000x increase in memory footprint. The team must provision the additional memory and CPU before the migration, or use a queueing pattern to throttle logins during peak hours. The attacker cost rises by the same 64-128x factor, which is the security justification.
Sample Input and Output
Input: Password: "hunter2" Salt: 16 random bytes Output: Salt (16 B, hex): 4f3a2b1c0d9e8f7a6b5c4d3e2f1a0b9c PBKDF2-HMAC-SHA-256 wall time at progressive iteration counts: PBKDF2 1000 iters: a1b2c3d4e5f60718... PBKDF2 iters= 1000 elapsed= 0 ms PBKDF2 10000 iters: a1b2c3d4e5f60718... PBKDF2 iters= 10000 elapsed= 4 ms PBKDF2 100000 iters: a1b2c3d4e5f60718... PBKDF2 iters= 100000 elapsed= 42 ms PBKDF2 600000 iters: a1b2c3d4e5f60718... PBKDF2 iters= 600000 elapsed= 251 ms Bouncy Castle (bcprov-jdk18on.jar) API summary: SCrypt.generate(pw, salt, N=32768, r=8, p=1, dkLen=32) BCrypt.hashpw(pw, BCrypt.gensalt(cost=12)) Argon2BytesGenerator + Argon2Parameters (m=19456, t=2, p=1) Expected wall times on a modern x86-64 CPU: PBKDF2 600,000 iters ~250 ms (CPU-only, GPU-friendly) bcrypt cost=12 ~250 ms (4 KB memory, GPU-friendly) scrypt N=32768,r=8 ~100 ms (32 MiB memory, GPU-resistant) Argon2 m=19456,t=2,p=1 ~200 ms (19 MiB memory, GPU-resistant)
The salt is a fresh 16-byte random value generated by SecureRandom at startup. The four PBKDF2 measurements show the linear scaling of wall time with iteration count: 0 ms at 1,000 iterations (too fast to measure accurately), 4 ms at 10,000, 42 ms at 100,000, and 251 ms at 600,000. The 251 ms wall time at 600,000 iterations matches the OWASP 2023 minimum and is the right baseline for any new PBKDF2 deployment. The Bouncy Castle summary lists the API entry points for scrypt, bcrypt, and Argon2 that would be measured by the same harness if Bouncy Castle were on the classpath; the expected wall times are the published values for the OWASP-recommended parameter sets on the same kind of CPU.
Common Mistakes to Avoid
First, do not measure KDF wall time without warming up the JIT. The first PBKDF2 invocation in a fresh JVM includes class loading, JIT interpretation, and method dispatch overhead that inflate the measured time by a factor of 2-10. Always run one warm-up derivation first, then measure; the demo's pbkdf2(password, salt, 1_000, 256) line before the loop is the warm-up.
Second, do not compare PBKDF2 at 1,000 iterations against Argon2 at the OWASP parameters. PBKDF2 at 1,000 iterations is the RFC 2898 default from 2000 and is six hundred times below the 2023 minimum; comparing it against Argon2 at 19 MiB memory is unfair to PBKDF2 and misrepresents the real trade-off. Always compare at the OWASP-recommended parameter sets: PBKDF2 at 600,000 iterations, bcrypt at cost 12, scrypt at N=32768 r=8 p=1, and Argon2id at m=19 MiB t=2 p=1.
Third, do not implement scrypt, bcrypt, or Argon2 yourself. All three have subtle constant-time arithmetic requirements, parameter validation, and side-channel defences that a hand-rolled implementation will miss. The published reference implementations (Bouncy Castle in Java, libsodium in C, the argon2-cffi bindings in Python) have been audited and validated against the official test vectors; use them.
Fourth, do not raise the iteration count or memory cost without measuring the wall time first. Doubling PBKDF2 iterations from 600,000 to 1.2 million doubles the login latency from 250 ms to 500 ms, which may exceed the application's login-latency budget. Always measure on the production hardware (or an equivalent) before deploying parameter changes, and roll out gradually with a feature flag so a misconfigured parameter does not lock every user out.
Real-World Applications
Spring Security 6 ships Argon2PasswordEncoder, PBKDF2PasswordEncoder, BCryptPasswordEncoder, and SCryptPasswordEncoder as built-in options, with the OWASP 2023 parameters as defaults and DelegatingPasswordEncoder for lazy migration. Spring Boot applications that include spring-boot-starter-security and Bouncy Castle on the classpath get all four KDFs out of the box.
Django 4.2 ships PBKDF2-HMAC-SHA-256 with 720,000 iterations as the default password hasher, with bcrypt, scrypt, and Argon2 available via the corresponding pip packages. Rails 7 ships bcrypt as the default (via the bcrypt gem) with PBKDF2 and Argon2 available via the corresponding gems. The Apache Shiro Java security framework ships PasswordMatcher with a configurable hash format that supports bcrypt, PBKDF2, scrypt, and Argon2 via Bouncy Castle.
For high-scale authentication services (Auth0, AWS Cognito, Okta), the KDF choice is driven by the per-login CPU and memory budget: Auth0 uses bcrypt cost 10 to keep per-login memory usage under 4 KB on shared infrastructure, AWS Cognito uses PBKDF2-HMAC-SHA-256 at 100,000 iterations as the default (below the OWASP 2023 minimum, but configurable up to 600,000 by the customer), and Okta uses a configurable mix of bcrypt and PBKDF2 depending on the tenant's compliance regime. The OWASP Password Storage Cheat Sheet (2023) tracks the current best practices and is updated as hardware advances make higher parameters necessary.
Conclusion
This tutorial walked through the key derivation comparison java topic in pure JDK code: benchmarking PBKDF2-HMAC-SHA-256 at 1,000, 10,000, 100,000, and 600,000 iterations to show the linear scaling of wall time with iteration count, and printing the Bouncy Castle classpath instructions and API entry points needed to add scrypt, bcrypt, and Argon2 to the same harness. To deepen your understanding, study the related topics of Argon2id (RFC 9106), the OWASP Password Storage Cheat Sheet (2023), and Spring Security's DelegatingPasswordEncoder for lazy migration between KDFs.
Comments
Comments
Post a Comment