SoFunction
Updated on 2025-04-06

Analysis of the principles of BCryptPasswordEncoder encryption and password verification in spring security

1. Encryption algorithm and hash algorithm

Some confidential information in many projects needs to be encrypted to protect the information of users or companies. At this time, this information will be exposed in encryption in the form of ciphertext.

The encryption algorithm is a reversible algorithm that uses certain rules to perform various calculations of the plain text to achieve the encryption effect.

  • The hash algorithm is irreversible. Common MD5 encryption uses hash algorithm for encryption. Encryption algorithms are reversible, so in many cases, encryption rules are very important. Once exposed, the plain text can be obtained by reverse deduction according to the rules. Therefore, encryption algorithms usually add salt, and symmetric encryption and asymmetric encryption are ones that add salt.
  • Although the hash algorithm is irreversible, it can be found by matching a lot of data through big data, so the hash algorithm also needs to add salt to ensure certain confidentiality.

2. The principle of BCryptPasswordEncoder encryption and decryption

BCryptPasswordEncoder encrypts the same data such as "11111". The results of each encryption are different. At this time, we need to think about a question: the same data is different each time it is encrypted. So how does it decrypt?

Let’s analyze the encryption source code of this method, refer to some information on the Internet:

3. Source code analysis

The BCryptPasswordEncoder class implements the PasswordEncoder interface, which defines two methods.

public interface PasswordEncoder {
    String encode(CharSequence rawPassword);
    boolean matches(CharSequence rawPassword, String encodedPassword);
}

where encode(...) is a method to encrypt strings, matches uses a method to verify whether the incoming plaintext password rawPassword matches the encoded Password.

That is, when encrypting the password, call encode, and when logging in and authenticating, call matches

Let's take a look at the specific implementation of these two methods in the BCryptPasswordEncoder class

1. encode method

public String encode(CharSequence rawPassword) {
    String salt;
    if (strength > 0) {
        if (random != null) {
            salt = (strength, random);
        }
        else {
            salt = (strength);
        }
    }
    else {
        salt = ();
    }
    return ((), salt);
}

It can be seen that in this method, a salt value is obtained based on a certain rule, and then the method is called, and the plaintext password and salt value are passed in. So let's look at what's done in the method

2. Method

public static String hashpw(String password, String salt) throws IllegalArgumentException {
        BCrypt B;
        String real_salt;
        byte passwordb[], saltb[], hashed[];
        char minor = (char) 0;
        int rounds, off = 0;
        StringBuilder rs = new StringBuilder();
        if (salt == null) {
            throw new IllegalArgumentException("salt cannot be null");
        }
        int saltLength = ();
        if (saltLength < 28) {
            throw new IllegalArgumentException("Invalid salt");
        }
        if ((0) != '$' || (1) != '2') {
            throw new IllegalArgumentException("Invalid salt version");
        }
        if ((2) == '$') {
            off = 3;
        }
        else {
            minor = (2);
            if (minor != 'a' || (3) != '$') {
                throw new IllegalArgumentException("Invalid salt revision");
            }
            off = 4;
        }
        if (saltLength - off < 25) {
            throw new IllegalArgumentException("Invalid salt");
        }
        // Extract number of rounds
        if ((off + 2) > '$') {
            throw new IllegalArgumentException("Missing salt rounds");
        }
        rounds = ((off, off + 2));
        real_salt = (off + 3, off + 25);
        try {
            passwordb = (password + (minor >= 'a' ? "\000" : "")).getBytes("UTF-8");
        }
        catch (UnsupportedEncodingException uee) {
            throw new AssertionError("UTF-8 is not supported");
        }
        saltb = decode_base64(real_salt, BCRYPT_SALT_LEN);
        B = new BCrypt();
        hashed = B.crypt_raw(passwordb, saltb, rounds);
        ("$2");
        if (minor >= 'a') {
            (minor);
        }
        ("$");
        if (rounds < 10) {
            ("0");
        }
        (rounds);
        ("$");
        encode_base64(saltb, , rs);
        encode_base64(hashed, bf_crypt_ciphertext.length * 4 - 1, rs);
        return ();
    }

You can see that in this method, it is first based on the incoming salt value.salt, and then get from salt based on some rulereal_salt,The subsequent operations are performed using this real_salt, and finally the encrypted string is obtained.

So here is a point: the incoming salt valuesaltIt is not the salt that is finally used to encrypt. The method is obtained through salt.real_salt, remember this because this is used in the subsequent matching method matches.

3. Matches method

The matches method is used to determine whether a plaintext corresponds to an encrypted string.

public boolean matches(CharSequence rawPassword, String encodedPassword) {
    if (encodedPassword == null || () == 0) {
        ("Empty encoded password");
        return false;
    }
    if (!BCRYPT_PATTERN.matcher(encodedPassword).matches()) {
        ("Encoded password does not look like BCrypt");
        return false;
    }
    return ((), encodedPassword);
}

In this method, some verification is performed on the ciphertext string. If the rules do not match, it will directly return a mismatch, and then the verification method is called. The first parameter is plaintext, and the second parameter is the encrypted string.

public static boolean checkpw(String plaintext, String hashed) {
    return equalsNoEarlyReturn(hashed, hashpw(plaintext, hashed));
}
static boolean equalsNoEarlyReturn(String a, String b) {
    char[] caa = ();
    char[] cab = ();
    if ( != ) {
        return false;
    }
    byte ret = 0;
    for (int i = 0; i < ; i++) {
        ret |= caa[i] ^ cab[i];
    }
    return ret == 0;
}

Notice:

equalsNoEarlyReturn(hashed, hashpw(plaintext, hashed))

Here, the first parameter is the encrypted string, while the second parameter is to encrypt the plain text string using the hashpw method mentioned just now.

hashpw(plaintext, hashed)

The first parameter is plain text, and the second parameter is an encrypted string, but it is passed in as a salt value salt, so I used the hashpw that I just mentioned to get inside the passed salt.real_salt, this ensures that the encryption of the plain text to be verified and the encryption of the existing ciphertext is used to obtain the same encryption strategy, and the algorithm and salt values ​​are the same. In this way, if the newly generated ciphertext is the same as the original ciphertext, the plain text strings corresponding to the two ciphertexts are equal.

This also shows that the salt value used during encryption is written in the final generated encryption string.

Summarize

The above is personal experience. I hope you can give you a reference and I hope you can support me more.