skip to content

What are the common correctness and security pitfalls when implementing PBKDF2 with SecretKeyFactory in Java?

level: seniorimportance: should knowfreq 45%

answer

  1. keyLength = bits (256, not 32)
  2. char[] + clearPassword in finally
  3. fresh SecureRandom salt, high stored iteration count
  4. checked: NoSuchAlgorithmException / InvalidKeySpecException
  5. MessageDigest.isEqual; SHA256 not SHA1; don't roll your own

basics

~20 s

Watch for: keyLength in bits not bytes, passing the password as char[] and wiping it, a fresh random salt per user, a high iteration count, handling NoSuchAlgorithmException/InvalidKeySpecException, and comparing hashes in constant time. Also store the parameters so you can upgrade later.

solid answer

~50 s

The PBKDF2 API has several easy-to-miss traps. The keyLength argument to PBEKeySpec is in bits - pass 256, not 32. The password must be a char[], and you should clearPassword() / zero it after use rather than holding a String. Generate a fresh SecureRandom salt per password; never a shared constant. Pick a high iteration count tuned to your hardware, and store it (plus salt and algorithm) so you can raise it later. getInstance throws NoSuchAlgorithmException and generateSecret throws InvalidKeySpecException - both checked - so handle/wrap them, but never log the password or derived bytes in the catch. Verify with MessageDigest.isEqual for constant-time comparison. Prefer PBKDF2WithHmacSHA256/512 over the legacy SHA1 variant. Finally, avoid a subtle interop bug: PBKDF2WithHmacSHA256 internally uses SHA-256 as its PRF regardless of keyLength, so the requested key length is independent of the PRF's digest size.

code

java · 14 lines
java
byte[] derive(char[] password, byte[] salt, int iterations) {
    PBEKeySpec spec = new PBEKeySpec(password, salt, iterations, 256); // BITS
    try {
        return SecretKeyFactory
            .getInstance("PBKDF2WithHmacSHA256") // NoSuchAlgorithmException (checked)
            .generateSecret(spec)                // InvalidKeySpecException (checked)
            .getEncoded();
    } catch (java.security.NoSuchAlgorithmException | java.security.spec.InvalidKeySpecException e) {
        throw new IllegalStateException("password hashing failed", e); // no secrets logged; fail closed
    } finally {
        spec.clearPassword();
        java.util.Arrays.fill(password, '\0');
    }
}

go deeper

for a junior

Recognizes that keyLength is bits and that you must catch the checked exceptions.

for a middle

Avoids the String/byte-length traps, uses a per-user salt and a real iteration count, and handles exceptions without leaking secrets.

for a senior

Knows constant-time compare, the PRF-vs-keyLength subtlety, fail-closed error handling, and not to hand-roll the loop.

for a principal

Codifies these as review/lint rules and a shared hashing component, owns the iteration-count uplift policy, and audits for auth-bypass-on-exception patterns across services.

## Why this matters The PBKDF2 API is small but has sharp edges; most production password bugs are not 'wrong algorithm' but 'right algorithm used slightly wrong'. Here is the full pitfall catalog with the reasoning behind each. ## 1. keyLength is in bits `new PBEKeySpec(pw, salt, iters, keyLength)` - `keyLength` is **bits**. Developers expecting bytes pass `32` and silently get a **32-bit** (4-byte) key, which is far too short and easy to brute-force. For a 32-byte key, pass `256`. ## 2. Password as char[], and wipe it Use a `char[]`, not a `String`. A `String` is immutable and can persist in memory (and the constant pool if literal) until garbage collection - you cannot deterministically erase it, widening the window an attacker with a memory dump could exploit. A `char[]` can be zeroed: call `spec.clearPassword()` (wipes the spec's copy) and `Arrays.fill(pw, '\0')` on your own array, ideally in a `finally`. ## 3. Salt: fresh, random, per password Generate with `SecureRandom`, 16 bytes, unique per password. A shared/constant salt re-enables rainbow tables and reveals equal passwords. Don't derive it from the username (predictable; breaks on rename). ## 4. Iteration count: high and stored The iteration count is your CPU work factor. Too low (e.g. 1,000) and cracking is cheap; modern guidance is in the hundreds of thousands for SHA-256, tuned so one hash takes ~tens of ms on your hardware. **Store it** with the hash so you can raise it over time (rehash on login). A hardcoded constant that you can never change is a trap. ## 5. Checked exceptions - handle without leaking - `SecretKeyFactory.getInstance(...)` -> `NoSuchAlgorithmException` (the named algorithm isn't available in any provider). - `factory.generateSecret(spec)` -> `InvalidKeySpecException` (bad spec parameters). Both are **checked**; you must catch or declare them. Wrap them in a domain exception, but **never put the password or derived bytes in the message or log**. Also avoid catching and silently returning a constant - that can turn a config error into an auth bypass. ## 6. Constant-time verification Compare the candidate and stored hash with `MessageDigest.isEqual`, not `Arrays.equals`/`==`. The former runs in time independent of the first mismatch position, closing a timing side-channel. ## 7. Algorithm choice Use `PBKDF2WithHmacSHA256` or `...SHA512`; avoid the legacy `PBKDF2WithHmacSHA1`. SHA-512 can be marginally better on 64-bit servers but SHA-256 is the common default. ## 8. The PRF-vs-keyLength subtlety `PBKDF2WithHmacSHA256` always uses **HMAC-SHA-256 as its internal PRF**; `keyLength` only controls how many output bytes PBKDF2 emits (PBKDF2 can stretch output beyond the PRF's digest size by running more blocks). So asking for 512 bits from the SHA-256 variant is legal but does **not** make it 'SHA-512' - it just runs the PRF more times. Don't conflate the PRF digest size with the requested key length. Note historically there are interop bugs where some libraries truncated/handled the password encoding differently; stick to one library/encoding for a given stored hash. ## 9. Don't roll your own loop Don't reimplement the PBKDF2 iteration loop or build it out of raw `MessageDigest` - use `SecretKeyFactory`. Hand-rolled crypto invites off-by-one and timing bugs. ## Putting it together A correct implementation: SecureRandom salt -> PBEKeySpec(char[], salt, highIters, 256) -> SecretKeyFactory('PBKDF2WithHmacSHA256').generateSecret -> getEncoded -> store algo+iters+salt+hash -> verify by recompute + MessageDigest.isEqual -> clear the char[] in finally -> rehash on login when params are outdated.

  • If you call PBKDF2WithHmacSHA256 with keyLength 512, do you get a SHA-512-strength hash?
    No. The variant always uses HMAC-SHA-256 as its internal PRF; keyLength only sets how many output bytes PBKDF2 emits by running extra blocks. Requesting 512 bits from the SHA-256 variant just produces more SHA-256-derived output, not SHA-512 security. To use SHA-512, ask for PBKDF2WithHmacSHA512.
  • Which exceptions must you handle, and what's the danger in the catch block?
    NoSuchAlgorithmException from getInstance and InvalidKeySpecException from generateSecret - both checked. The danger is logging the password or derived key, and silently returning a constant/true on failure, which can convert a misconfiguration into an authentication bypass. Wrap in a domain exception, fail closed, and log no secrets.

saying these in an interview costs you the question

  • Passing keyLength as bytes (e.g. 32) expecting a 32-byte key
  • Holding the password in a String and never clearing it
  • Logging the password or derived bytes in a catch block
  • Swallowing NoSuchAlgorithmException and returning a default that bypasses auth
  • Reimplementing the PBKDF2 loop by hand instead of using SecretKeyFactory

context