There is a tool class BCryptPasswordEncoder for password encryption in Spring Security, which is very simple and interesting to use. Let’s see how it works.

Usage of BCryptPasswordEncoder

First create a Spring Boot project and add the Spring Security dependency. Then create a test class with the following code.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
final private String password = "123456";
 
@Test
public void TestCrypt()
{
    BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder();
 
    String encode1 = bCryptPasswordEncoder.encode(password);
    System.out.println("encode1:" + encode1);
    
    String encode2 = bCryptPasswordEncoder.encode(password);
    System.out.println("encode2:" + encode2);
}

In the above code, a BCryptPasswordEncoder class is first instantiated and then the same plaintext string is encrypted and output using the encode method of the class. Run the above code and the output result is as follows.

1
2
encode1:$2a$10$SqbQb0pD3KYrH7ZVTWdRZOhPAelQqa..lUnysXoWag6RvMkyC5SE6
encode2:$2a$10$0sjBLlwrrch2EjgYls197e9dGRCMbQ7KUIt/ODPTSU0W.mEPaGkfG

As you can see from the above output, the same plaintext is encrypted twice, but different results are output. Isn’t it amazing? But there is a problem, if you use BCryptPasswordEncoder to encrypt the login password, can it still be verified? Of course it is possible to verify.

To verify, you use the matches method of BCryptPasswordEncoder, and the code is as follows.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
final private String password = "123456";
 
@Test
public void TestCrypt()
{
    BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder();
 
    String encode1 = bCryptPasswordEncoder.encode(password);
    System.out.println("encode1:" + encode1);
 
    boolean matches1 = bCryptPasswordEncoder.matches(password, encode1);
    System.out.println("matches1:" + matches1);
 
    String encode2 = bCryptPasswordEncoder.encode(password);
    System.out.println("encode2:" + encode2);
 
    boolean matches2 = bCryptPasswordEncoder.matches(password, encode2);
    System.out.println("matches2:" + matches2);
}

Use the matches method to verify that the plaintext before encryption and the ciphertext after encryption match. The output is as follows.

1
2
3
4
encode1:$2a$10$qxU.rFLeTmZg47FyqJlZwu.QNX9RpEvqBUJiwUvUE0p4ENR.EndfS
matches1:true
encode2:$2a$10$NyGEOsQ1Hxv2gvYRmaEENueORlVDtSqoB/fHN76KkvQDeg7fbTy22
matches2:true

You can see that the two encrypted strings are different, but both can successfully match the original plaintext password “123456” by matches method. Again, this is amazing, why is this?

How the encode and matches methods work

Let’s take a look at how they work through the source code.

First we download the dependent source code locally, so that we can debug it with IDEA. First set a breakpoint at the encode method to see how encode is implemented, the code is as follows.

1
2
3
4
5
6
7
8
@Override
public String encode(CharSequence rawPassword) {
  if (rawPassword == null) {
    throw new IllegalArgumentException("rawPassword cannot be null");
  }
  String salt = getSalt();
  return BCrypt.hashpw(rawPassword.toString(), salt);
}

Run directly to the return statement and look at the value of salt, which is as follows.

1
$2a$10$H73jYFFLWWf.VV/mwonqru

Then proceed to the BCrypt.hashpw method, which has the following code.

1
2
3
4
5
6
7
public static String hashpw(String password, String salt) {
  byte passwordb[];
 
  passwordb = password.getBytes(StandardCharsets.UTF_8);
 
  return hashpw(passwordb, salt);
}

The focus of this method is also the hashpw method, which does nothing and continues into the hashpw method. The code is as follows.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
public static String hashpw(byte passwordb[], String salt) {
  BCrypt B;
  String real_salt;
  byte saltb[], hashed[];
  char minor = (char) 0;
  int rounds, off;
  StringBuilder rs = new StringBuilder();
 
  if (salt == null) {
    throw new IllegalArgumentException("salt cannot be null");
  }
 
  int saltLength = salt.length();
 
  if (saltLength < 28) {
    throw new IllegalArgumentException("Invalid salt");
  }
 
  if (salt.charAt(0) != '$' || salt.charAt(1) != '2') {
    throw new IllegalArgumentException("Invalid salt version");
  }
  if (salt.charAt(2) == '$') {
    off = 3;
  }
  else {
    minor = salt.charAt(2);
    if ((minor != 'a' && minor != 'x' && minor != 'y' && minor != 'b') || salt.charAt(3) != '$') {
      throw new IllegalArgumentException("Invalid salt revision");
    }
    off = 4;
  }
 
  // Extract number of rounds
  if (salt.charAt(off + 2) > '$') {
    throw new IllegalArgumentException("Missing salt rounds");
  }
 
  if (off == 4 && saltLength < 29) {
    throw new IllegalArgumentException("Invalid salt");
  }
  rounds = Integer.parseInt(salt.substring(off, off + 2));
 
  real_salt = salt.substring(off + 3, off + 25);
  saltb = decode_base64(real_salt, BCRYPT_SALT_LEN);
 
  if (minor >= 'a') {
    passwordb = Arrays.copyOf(passwordb, passwordb.length + 1);
  }
 
  B = new BCrypt();
  hashed = B.crypt_raw(passwordb, saltb, rounds, minor == 'x', minor == 'a' ? 0x10000 : 0);
 
  rs.append("$2");
  if (minor >= 'a') {
    rs.append(minor);
  }
  rs.append("$");
  if (rounds < 10) {
    rs.append("0");
  }
  rs.append(rounds);
  rs.append("$");
  encode_base64(saltb, saltb.length, rs);
  encode_base64(hashed, bf_crypt_ciphertext.length * 4 - 1, rs);
  return rs.toString();
}

The code is very long, but the real key code is in the position of lines 43, 44 and 51 above. Line 43 gets the real salt, line 44 uses base64 for decoding, and line 51 uses the password and salt for processing. The return value is rs, in lines 63 and 64, the salt is base64 encoded and put into rs, then the hashed is base64 encoded and put into rs, and finally returned by rs.toString().

Although the above code is long, in fact, the only key thing is the above sentences I mentioned, don’t need to read the rest. Let’s look at the source code of matches, the code is as follows.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
@Override
public boolean matches(CharSequence rawPassword, String encodedPassword) {
  if (rawPassword == null) {
    throw new IllegalArgumentException("rawPassword cannot be null");
  }
  if (encodedPassword == null || encodedPassword.length() == 0) {
    this.logger.warn("Empty encoded password");
    return false;
  }
  if (!this.BCRYPT_PATTERN.matcher(encodedPassword).matches()) {
    this.logger.warn("Encoded password does not look like BCrypt");
    return false;
  }
  return BCrypt.checkpw(rawPassword.toString(), encodedPassword);
}

At the return on line 14, the BCrypt.checkpw method is called here. rawPassword.toString() is our password, which is “123456”, followed by encodedPassword, which is our encrypted password, and continues into the checkpw method, the code is as follows.

1
2
3
public static boolean checkpw(String plaintext, String hashed) {
  return equalsNoEarlyReturn(hashed, hashpw(plaintext, hashed));
}

There is only one line of code here, but the code also calls the hashpw method, the parameters passed in are plaintext and hashed, plaintext is our password, i.e. “123456”, hashed is the encrypted password. Here we basically understand. After hashed enters the hashpw function, it takes out the real salt through the code in line 43, and then encrypts it with our password, so that the process is connected.

Summary

When I saw that BCryptPasswordEncoder could generate different ciphertexts for the same password and also match them by matches method, I thought it was amazing. After debugging, we found that the ciphertext contains a lot of information, including the salt and hash after encryption with salt, because each time the salt is different, so each time the hash is also different. This way, the same plaintext can generate different ciphertexts, and the ciphertext contains both salt and hash, so the verification process and generation process are the same.

A rough flowchart of the call relationship is attached for your reference.

flowchart

Reference https://blog.csdn.net/easysec/article/details/121719949