← All articles

Generating and hashing passwords in Java

The JDK gives you a solid CSPRNG and nothing for password hashing, so the real decision is which library to put on the classpath — and how to keep passwords out of String.

1. Generate a password

java.security.SecureRandom is a CSPRNG backed by the operating system. nextInt(bound) already avoids modulo bias internally:

java
import java.security.SecureRandom;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

public final class Passwords {

    // One shared instance is fine and is thread-safe; creating a SecureRandom per
    // call is wasteful and can block on some platforms.
    private static final SecureRandom RANDOM = new SecureRandom();

    private static final String UPPER = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
    private static final String LOWER = "abcdefghijklmnopqrstuvwxyz";
    private static final String DIGITS = "0123456789";
    private static final String SYMBOLS = "!@#$%^&*()-_=+[]{};:,.?/~";

    public static String generate(int length, String alphabet) {
        if (length < 1 || alphabet.isEmpty()) {
            throw new IllegalArgumentException("length and alphabet must be non-empty");
        }
        StringBuilder sb = new StringBuilder(length);
        for (int i = 0; i < length; i++) {
            sb.append(alphabet.charAt(RANDOM.nextInt(alphabet.length())));
        }
        return sb.toString();
    }

    /** Same, but guaranteed to contain at least one character of each set. */
    public static String generateStructured(int length) {
        String[] sets = { UPPER, LOWER, DIGITS, SYMBOLS };
        if (length < sets.length) {
            throw new IllegalArgumentException("length must be at least " + sets.length);
        }

        List<Character> chars = new ArrayList<>(length);
        for (String set : sets) {
            chars.add(set.charAt(RANDOM.nextInt(set.length())));
        }

        String pool = String.join("", sets);
        while (chars.size() < length) {
            chars.add(pool.charAt(RANDOM.nextInt(pool.length())));
        }

        Collections.shuffle(chars, RANDOM); // shuffle with the CSPRNG, not a default Random
        StringBuilder sb = new StringBuilder(length);
        for (char c : chars) {
            sb.append(c);
        }
        return sb.toString();
    }
}

2. Hash with Argon2id

Two common options. Spring Security is the least code if you already use it — note that spring-security-crypto needs BouncyCastle on the classpath for the Argon2 encoder:

gradle
implementation "org.springframework.security:spring-security-crypto:6.3.3"
implementation "org.bouncycastle:bcprov-jdk18on:1.78.1"
java
import org.springframework.security.crypto.argon2.Argon2PasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

// Sensible defaults for the current Spring Security line.
PasswordEncoder argon2 = Argon2PasswordEncoder.defaultsForSpringSecurity_v5_8();

String hash = argon2.encode(password);
// $argon2id$v=19$m=16384,t=2,p=1$...  <- inspect and raise the parameters if you can

boolean ok = argon2.matches(candidate, hash);

If you would rather control the parameters explicitly, argon2-jvm maps directly onto the reference C implementation:

gradle
implementation "de.mkammerer:argon2-jvm:2.11"
java
import de.mkammerer.argon2.Argon2;
import de.mkammerer.argon2.Argon2Factory;

Argon2 argon2 = Argon2Factory.create(Argon2Factory.Argon2Types.ARGON2id, 16, 32);

// hash(iterations, memoryInKiB, parallelism, password)
String hash = argon2.hash(3, 65536, 4, password.toCharArray());

boolean ok = argon2.verify(hash, candidate.toCharArray());
argon2.wipeArray(passwordChars);   // clear the char[] when you are done with it

3. Hash with bcrypt

Spring Security's BCrypt encoder is the usual choice. Its default strength is 10; raise it:

java
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;

// strength 12 → roughly 200-400 ms per hash on a modern core.
BCryptPasswordEncoder bcrypt = new BCryptPasswordEncoder(12);

String hash = bcrypt.encode(password);            // $2a$12$...
boolean ok = bcrypt.matches(candidate, hash);

bcrypt reads a maximum of 72 bytes. Anything past that is silently ignored, so a long generated passphrase and a shorter one sharing the first 72 bytes verify identically.

4. Verify, and migrate while you are there

This final part uses the encoder and argon2 objects created above:

java
String stored = user.getPasswordHash();

if (stored == null || !encoder.matches(candidate, stored)) {
    // Identical response whether the user exists or the password is wrong.
    throw new BadCredentialsException("Invalid credentials");
}

// Upgrading a legacy bcrypt row to Argon2id on the user's next login.
if (stored.startsWith("$2a$") || stored.startsWith("$2b$")) {
    user.setPasswordHash(argon2.encode(candidate));
    users.save(user);
} else if (encoder.upgradeEncoding(stored)) {
    user.setPasswordHash(encoder.encode(candidate));
    users.save(user);
}

Pitfalls in Java

  • java.util.Random — and Math.random() — is a 48-bit LCG. Given a couple of outputs you can predict the rest.
  • MessageDigest.getInstance("SHA-256") for passwords: it is a fast hash, and a single round with no salt is worse than a long random password with no hashing in some threat models.
  • String is immutable, so a password held in a String stays in the heap until the garbage collector gets to it. Where the API allows it, use char[] and wipe it afterwards.
  • Default strengths. BCryptPasswordEncoder defaults to 10 and Spring's Argon2 defaults are modest. Check what you are actually using rather than assuming.
  • Missing BouncyCastle turns into a runtime failure in production, not a compile error. Add the dependency for the environment you deploy to as well.
  • Logging. Framework logs love to print the credentials object or the request body. Make sure login requests never reach a log appender.

A quick sanity check

When you change parameters, it is useful to confirm that the hash your application produced really carries the values you intended. The Hash tab shows the parsed Argon2id parameters in the PHC string, and the Verify tab will check a Java-produced bcrypt or Argon2id hash against a password.