PasswordEncoder and BCrypt

The PasswordEncoder Interface

public interface PasswordEncoder {
    String encode(CharSequence rawPassword);                          // at registration
    boolean matches(CharSequence rawPassword, String encodedPassword); // at login
    default boolean upgradeEncoding(String encodedPassword) { return false; }
}
  • encode creates a new random salt each time, so the same password gives a different hash every call.
  • matches reads the salt from the stored hash, hashes the typed password the same way and compares in constant time.
  • You never decode a password. There is no decode method.

Anatomy of a BCrypt Hash

Example

A BCrypt hash is one 60-character string that contains everything needed to verify it: version, cost and salt. That is why you need only one database column:

public class Main {
    public static void main(String[] args) {
        // What new BCryptPasswordEncoder().encode("asha123") stores (example value)
        String hash = "$2a$10$N9qo8uLOickgx2ZMRZoMyeIjZAgcfl7p92ldGxad68LJZdL17lhWy";

        String[] parts = hash.split("\\$");          // ["", "2a", "10", salt+hash]
        String version = parts[1];
        int cost = Integer.parseInt(parts[2]);
        String salt = parts[3].substring(0, 22);
        String digest = parts[3].substring(22);

        System.out.println("length  : " + hash.length());
        System.out.println("version : " + version);
        System.out.println("cost    : " + cost + "  -> 2^" + cost + " = " + (1 << cost) + " rounds");
        System.out.println("salt    : " + salt + "  (22 chars, 128 bits)");
        System.out.println("hash    : " + digest + "  (31 chars)");
        System.out.println("cost 12 is " + ((1 << 12) / (1 << 10)) + "x slower than cost 10");
    }
}

Output

length  : 60
version : 2a
cost    : 10  -> 2^10 = 1024 rounds
salt    : N9qo8uLOickgx2ZMRZoMye  (22 chars, 128 bits)
hash    : IjZAgcfl7p92ldGxad68LJZdL17lhWy  (31 chars)
cost 12 is 4x slower than cost 10

Using BCrypt in Spring

@Bean
PasswordEncoder passwordEncoder() {
    return new BCryptPasswordEncoder(12);   // cost 12: about 250 ms per hash on a typical server
}

// in a service
String hash = passwordEncoder.encode("asha123");          // $2a$12$...
boolean ok  = passwordEncoder.matches("asha123", hash);  // true
CostRoundsApprox. time per hash
10 (default)1,024~60 ms
124,096~250 ms
1416,384~1 s (too slow for most logins)

Tip: Choose the highest cost that keeps a login under about 250–500 ms on your production servers. BCrypt only uses the first 72 bytes of a password; that is fine for normal passwords.

DelegatingPasswordEncoder

Algorithms age. PasswordEncoderFactories.createDelegatingPasswordEncoder() stores the algorithm id as a prefix, so old hashes keep working while new ones use the current default (BCrypt):

{bcrypt}$2a$10$dXJ3SW6G7P50lGmMkkmwe.20cQQubK3.HZWzG3YB1tlRy.fqvM/BG
{pbkdf2}5d923b44a6d129f3ddf3e3c8d29412723dcbde72445e8ef6bf3b508fbf17fa4ed4d6b99ca763d8dc
{noop}plaintext      <- only for tests, never in production
@Bean
PasswordEncoder passwordEncoder() {
    return PasswordEncoderFactories.createDelegatingPasswordEncoder();
}

Note: A stored password without a {id} prefix makes the delegating encoder throw "There is no PasswordEncoder mapped for the id null". Either use the delegating encoder everywhere or a plain BCryptPasswordEncoder, consistently.