SecureRandom, PBKDF2 & Password-Safe Key Derivation in Java

Cryptography depends on high-quality randomness. Java's java.security.SecureRandom provides a cryptographically strong random number generator seeded from OS entropy. PBKDF2 (Password-Based Key Derivation Function 2) turns a low-entropy password into a fixed-length cryptographic key using a salt and many iterations to resist brute-force attacks.

Share

SecureRandom: Cryptographically Secure Randomness

java.util.Random and Math.random() are not suitable for cryptography: they are deterministic and predictable. Always use SecureRandom for security-sensitive values.

Java
import java.security.SecureRandom;
import java.util.HexFormat;

// Default SecureRandom — uses OS entropy source (/dev/urandom on Linux)
SecureRandom random = new SecureRandom();

// Generate a 16-byte random salt
byte[] salt = new byte[16];
random.nextBytes(salt);
System.out.println("Salt: " + HexFormat.of().formatHex(salt));

// Generate a random int in range [0, 100)
int n = random.nextInt(100);

// Generate a random long
long token = random.nextLong();

// Generate a strong instance (may block on Linux until entropy is available)
SecureRandom strong = SecureRandom.getInstanceStrong();
MethodWhen to use
new SecureRandom()Default: use for most cryptographic operations
SecureRandom.getInstanceStrong()Highest quality entropy; may block on low-entropy systems (avoid in containers at startup)
SecureRandom.getInstance("SHA1PRNG")Specific algorithm: rarely needed; prefer default

Common SecureRandom Use Cases

Java
SecureRandom sr = new SecureRandom();

// 1. Generate a random session token (URL-safe Base64)
byte[] tokenBytes = new byte[32];
sr.nextBytes(tokenBytes);
String sessionToken = Base64.getUrlEncoder().withoutPadding().encodeToString(tokenBytes);
System.out.println("Token: " + sessionToken);   // 43-character URL-safe string

// 2. Generate a random password reset code (6-digit OTP)
int otp = 100_000 + sr.nextInt(900_000);         // always 6 digits
System.out.println("OTP: " + otp);

// 3. Generate a random UUID (v4 uses SecureRandom internally)
java.util.UUID uuid = java.util.UUID.randomUUID();

// 4. Generate a random AES IV
byte[] iv = new byte[12];
sr.nextBytes(iv);

// 5. Generate a random salt for password hashing
byte[] salt = new byte[16];
sr.nextBytes(salt);

Why Salts Are Essential for Password Hashing

Without a salt, identical passwords produce identical hashes: attackers can pre-compute hashes for common passwords (rainbow tables). A unique random salt per user prevents this.

ScenarioWithout SaltWith Salt
Two users with same passwordIdentical hashes: one breach exposes bothDifferent hashes: neither is revealed
Rainbow table attackInstant lookupImpossible: must brute-force with each unique salt
Dictionary attackPre-compute once, reuseMust recompute for each target user
Salt storage-Store alongside hash: it's not a secret

PBKDF2WithHmacSHA256: Password Hashing

PBKDF2 applies HMAC many thousands of times, making brute-force attacks slow. The iteration count is the primary cost parameter: increase it as hardware gets faster.

Java
import javax.crypto.*;
import javax.crypto.spec.*;
import java.security.*;
import java.util.Base64;

public class Pbkdf2PasswordHash {

    private static final int ITERATIONS  = 310_000;  // OWASP 2023 for PBKDF2-SHA256
    private static final int KEY_LENGTH  = 256;       // bits (32 bytes)
    private static final int SALT_LENGTH = 16;        // bytes

    /**
     * Hash a password for storage.
     * Returns a storable string: "iterations:saltBase64:hashBase64"
     */
    public static String hashPassword(char[] password) throws Exception {
        byte[] salt = new byte[SALT_LENGTH];
        new SecureRandom().nextBytes(salt);

        byte[] hash = pbkdf2(password, salt, ITERATIONS, KEY_LENGTH);

        return ITERATIONS + ":"
             + Base64.getEncoder().encodeToString(salt) + ":"
             + Base64.getEncoder().encodeToString(hash);
    }

    /**
     * Verify a password against a stored hash string.
     */
    public static boolean verifyPassword(char[] password, String stored) throws Exception {
        String[] parts = stored.split(":");
        int    iterations = Integer.parseInt(parts[0]);
        byte[] salt       = Base64.getDecoder().decode(parts[1]);
        byte[] expected   = Base64.getDecoder().decode(parts[2]);

        byte[] computed = pbkdf2(password, salt, iterations, expected.length * 8);

        return MessageDigest.isEqual(computed, expected);   // constant-time
    }

    private static byte[] pbkdf2(char[] password, byte[] salt, int iterations, int keyLenBits)
            throws Exception {
        PBEKeySpec spec = new PBEKeySpec(password, salt, iterations, keyLenBits);
        try {
            SecretKeyFactory skf = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256");
            return skf.generateSecret(spec).getEncoded();
        } finally {
            spec.clearPassword();   // wipe password from memory
        }
    }

    public static void main(String[] args) throws Exception {
        char[] password = "correct horse battery staple".toCharArray();

        String stored = hashPassword(password);
        System.out.println("Stored: " + stored);

        System.out.println("Valid  : " + verifyPassword(password, stored));       // true
        System.out.println("Invalid: " + verifyPassword("wrong".toCharArray(), stored)); // false
    }
}
Always call spec.clearPassword() in a finally block to zero out the password char array in memory. Using char[] instead of String for passwords lets you control when the data is cleared: String is immutable and stays in the heap until GC.

Iteration Count: Tuning for Your Hardware

The iteration count should be calibrated so hashing takes ~100–300 ms on your login server. Use this benchmark to find the right value:

Java
public static void benchmarkIterations() throws Exception {
    char[] password = "benchmark".toCharArray();
    byte[] salt     = new byte[16];
    new SecureRandom().nextBytes(salt);

    int[] counts = {100_000, 200_000, 310_000, 500_000, 1_000_000};
    for (int iterations : counts) {
        long start = System.currentTimeMillis();
        PBEKeySpec spec = new PBEKeySpec(password, salt, iterations, 256);
        SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256").generateSecret(spec);
        spec.clearPassword();
        long elapsed = System.currentTimeMillis() - start;
        System.out.printf("iterations=%,d  time=%d ms%n", iterations, elapsed);
    }
}
OWASP Recommendation (2023)AlgorithmIterations
MinimumPBKDF2-SHA256600,000
MinimumPBKDF2-SHA512210,000
Typical productionPBKDF2-SHA256310,000 – 600,000

PBKDF2 vs bcrypt vs Argon2

AlgorithmJava supportMemory-hardNotes
PBKDF2-SHA256Built-in JCANoNIST-approved, FIPS-compliant; vulnerable to GPU attacks
bcryptThird-party (jBCrypt)ModerateMax 72-byte password; widely used; not FIPS
Argon2idThird-party (password4j, Bouncy Castle)YesWinner of PHC 2015; most resistant to GPU/ASIC; OWASP first choice
scryptBouncy CastleYesMemory-hard; complex parameter tuning

When FIPS compliance is not required and a third-party library is acceptable, prefer Argon2id. PBKDF2 is the right choice when you must stay within the JDK's built-in JCA providers.