04 Password Safety Which Encryption and Decryption Techniques Are Included in Spring Security

04 Password Safety Which Encryption and Decryption Techniques are Included in Spring Security #

Through the learning of the previous two lectures, I believe you have already mastered the user authentication system in Spring Security. The process of user authentication usually involves password verification, so password security is also a core issue that we need to consider. As a feature-complete security framework, Spring Security provides a PasswordEncoder component for performing authentication operations and also includes a standalone and complete encryption module for separate use in applications.

PasswordEncoder #

Let’s start by reviewing the entire user authentication process. In the AuthenticationProvider, we need to use the PasswordEncoder component to verify the correctness of the password, as shown in the figure below:

Drawing 0.png

Relationship between PasswordEncoder component and authentication process

In the lecture “User Authentication: How to Build a User Authentication System Using Spring Security?”, we also introduced a database-based user information storage scheme:

@Override

protected void configure(AuthenticationManagerBuilder auth) throws Exception {

 

        auth.jdbcAuthentication().dataSource(dataSource)

               .usersByUsernameQuery("select username, password, enabled from Users " + "where username=?")

               .authoritiesByUsernameQuery("select username, authority from UserAuthorities " + "where username=?")

               .passwordEncoder(new BCryptPasswordEncoder());

}

Please note that in the above method, we must integrate the encryption mechanism when verifying user information through the jdbcAuthentication() method, which means embedding an implementation class of the PasswordEncoder interface using the passwordEncoder() method.

PasswordEncoder Interface #

In Spring Security, the PasswordEncoder interface represents a password encoder, which is mainly used to specify the specific encryption method of the password and how to match and verify an encrypted string with plaintext. The PasswordEncoder interface is defined as follows:

public interface PasswordEncoder {

    // Encode the raw password

    String encode(CharSequence rawPassword);

    // Compare the submitted raw password with the encrypted password stored in the database

    boolean matches(CharSequence rawPassword, String encodedPassword);

    // Determine if the encrypted password needs to be upgraded again, and return false by default

    default boolean upgradeEncoding(String encodedPassword) {

        return false;

    }

}

Spring Security provides a large number of implementations of the PasswordEncoder interface, as shown in the following figure:

Drawing 1.png

PasswordEncoder implementations in Spring Security

We will now explain several common implementations of the PasswordEncoder interface shown in the figure above.

  • NoOpPasswordEncoder: Keeps the password in plaintext form and does not encode it. This PasswordEncoder is usually only used for demonstration purposes and should not be used in production environments.
  • StandardPasswordEncoder: Uses the SHA-256 algorithm to perform hashing on the password.
  • BCryptPasswordEncoder: Uses the bcrypt strong hashing algorithm to perform hashing on the password.
  • Pbkdf2PasswordEncoder: Uses the PBKDF2 algorithm to perform hashing on the password.

Now let’s take BCryptPasswordEncoder as an example and take a look at its encode method, as shown below:

public String encode(CharSequence rawPassword) {

        String salt;

        if (random != null) {

           salt = BCrypt.gensalt(version.getVersion(), strength, random);

        } else {

           salt = BCrypt.gensalt(version.getVersion(), strength);

        }

        return BCrypt.hashpw(rawPassword.toString(), salt);

}

As you can see, the above encode method performs two steps. First, it uses the BCrypt utility class provided by Spring Security to generate a salt, and then generates the final encrypted password based on the salt and plaintext password. It is necessary to elaborate on the concept of salting: salting means that when initializing plaintext data, the system automatically adds some additional data to this plaintext and then hashes it. The introduction of the salting mechanism is to further ensure the security of encrypted data. One-way hashing encryption and salting ideas are widely used in password generation and verification during the system login process.

Similarly, in Pbkdf2PasswordEncoder, the password is also hashed after being salted, and then the result is hashed again with the salt, repeating this process multiple times to generate the final encrypted password.

After introducing the basic structure of PasswordEncoder, let’s continue to look at its application. If we want to use a specific implementation class of PasswordEncoder in the application, usually we only need to create an instance through its constructor, for example:

PasswordEncoder p = new StandardPasswordEncoder(); 

PasswordEncoder p = new StandardPasswordEncoder("secret");

 

PasswordEncoder p = new SCryptPasswordEncoder(); 

PasswordEncoder p = new SCryptPasswordEncoder(16384, 8, 1, 32, 64);

And if we want to use NoOpPasswordEncoder, in addition to the constructor, we can also get a static instance through its getInstance() method, as shown below:

PasswordEncoder p = NoOpPasswordEncoder.getInstance()
#### Custom PasswordEncoder

Although Spring Security provides us with a rich set of PasswordEncoders, we can also implement our own password encoding and validation mechanisms by implementing this interface. For example, we can write a PlainTextPasswordEncoder as shown below:

```java
public class PlainTextPasswordEncoder implements PasswordEncoder {

   @Override
   public String encode(CharSequence rawPassword) {
         return rawPassword.toString(); 
   }

   @Override
   public boolean matches(CharSequence rawPassword, String encodedPassword) {
         return rawPassword.equals(encodedPassword); 
   }

}

The functionality of PlainTextPasswordEncoder is similar to NoOpPasswordEncoder, as it does not perform any operations on the plain text. If you want to use a specific algorithm to integrate with PasswordEncoder, you can implement a class similar to the following Sha512PasswordEncoder, which uses SHA-512 as the encryption algorithm:

public class Sha512PasswordEncoder implements PasswordEncoder {

   @Override
   public String encode(CharSequence rawPassword) {
      return hashWithSHA512(rawPassword.toString());
   }

   @Override
   public boolean matches(CharSequence rawPassword, String encodedPassword) {
      String hashedPassword = encode(rawPassword);
      return encodedPassword.equals(hashedPassword);
   }

   private String hashWithSHA512(String input) {
     StringBuilder result = new StringBuilder();
     try {
       MessageDigest md = MessageDigest.getInstance("SHA-512");
       byte [] digested = md.digest(input.getBytes());
       for (int i = 0; i < digested.length; i++) {
         result.append(Integer.toHexString(0xFF & digested[i]));
       }
     } catch (NoSuchAlgorithmException e) {
       throw new RuntimeException("Bad algorithm");
     }
     return result.toString();
  }

}

In the above code, the hashWithSHA512() method utilizes the one-way hash encryption algorithm mentioned earlier to generate a message digest. Its main characteristics include being one-way irreversible and having a fixed length for the cipher text. It also has the advantage of having a low probability of collisions, meaning that even a slight difference in the plaintext will result in completely different cipher text. SHA (Secure Hash Algorithm) and MD5 (Message Digest 5) are commonly used one-way hash encryption algorithms, and the JDK’s MessageDigest class already includes a default implementation. We can call its methods directly.

DelegatingPasswordEncoder #

In the previous discussion, we assumed that only one PasswordEncoder would be used during the password encryption and decryption process. If this PasswordEncoder does not meet our requirements, then we need to replace it with another PasswordEncoder. This raises the question of how to elegantly handle this change.

In a typical business system, replacing a component may not incur a high cost due to the constant changes in the system. However, for a mature development framework like Spring Security, changes in design and implementation should not happen frequently. Therefore, a balance needs to be maintained between the compatibility of the new/old PasswordEncoder, the stability and flexibility of the framework itself. To achieve this balance, Spring Security provides the DelegatingPasswordEncoder.

Although DelegatingPasswordEncoder also implements the PasswordEncoder interface, in fact, it plays more of a role as a proxy component, as can be seen from its name. DelegatingPasswordEncoder delegates the specific encoding implementation to different algorithms as required, to achieve compatibility between different encoding algorithms and coordinate changes, as shown in the following diagram:

Drawing 2.png

Diagram illustrating the proxy role of DelegatingPasswordEncoder

Let’s take a look at the constructor of the DelegatingPasswordEncoder class:

public DelegatingPasswordEncoder(String idForEncode,
        Map<String, PasswordEncoder> idToPasswordEncoder) {
        if (idForEncode == null) {
             throw new IllegalArgumentException("idForEncode cannot be null");
        }
        if (!idToPasswordEncoder.containsKey(idForEncode)) {
             throw new IllegalArgumentException("idForEncode " + idForEncode + "is not found in idToPasswordEncoder " + idToPasswordEncoder);
        }
        for (String id : idToPasswordEncoder.keySet()) {
             if (id == null) {
                 continue;
}
if (id.contains(PREFIX)) {

    throw new IllegalArgumentException("id " + id + " cannot contain " + PREFIX);

}

if (id.contains(SUFFIX)) {

    throw new IllegalArgumentException("id " + id + " cannot contain " + SUFFIX);

}

}

this.idForEncode = idForEncode;

this.passwordEncoderForEncode = idToPasswordEncoder.get(idForEncode);

this.idToPasswordEncoder = new HashMap<>(idToPasswordEncoder);

}

This constructor's `idForEncode` parameter determines the type of `PasswordEncoder`, while the `idToPasswordEncoder` parameter determines the compatible types for matching. Obviously, `idToPasswordEncoder` must contain the corresponding `idForEncode`.

Let's take a look at the entry point of this constructor. In Spring Security, there is a factory class `PasswordEncoderFactories` for creating `PasswordEncoder`, as follows:

```java
public class PasswordEncoderFactories {

    @SuppressWarnings("deprecation")
    
    public static PasswordEncoder createDelegatingPasswordEncoder() {

        String encodingId = "bcrypt";

        Map<String, PasswordEncoder> encoders = new HashMap<>();

        encoders.put(encodingId, new BCryptPasswordEncoder());

        encoders.put("ldap", new org.springframework.security.crypto.password.LdapShaPasswordEncoder());

        encoders.put("MD4", new org.springframework.security.crypto.password.Md4PasswordEncoder());

        encoders.put("MD5", new org.springframework.security.crypto.password.MessageDigestPasswordEncoder("MD5"));

        encoders.put("noop", org.springframework.security.crypto.password.NoOpPasswordEncoder.getInstance());

        encoders.put("pbkdf2", new Pbkdf2PasswordEncoder());

        encoders.put("scrypt", new SCryptPasswordEncoder());

        encoders.put("SHA-1", new org.springframework.security.crypto.password.MessageDigestPasswordEncoder("SHA-1"));

        encoders.put("SHA-256", new org.springframework.security.crypto.password.MessageDigestPasswordEncoder("SHA-256"));

        encoders.put("sha256", new org.springframework.security.crypto.password.StandardPasswordEncoder());

        encoders.put("argon2", new Argon2PasswordEncoder());

 

        return new DelegatingPasswordEncoder(encodingId, encoders);

    }

 

    private PasswordEncoderFactories() {}

}

As you can see, this factory class initializes a Map that contains all the supported PasswordEncoders in Spring Security. Moreover, it is clear that the default one used by the framework is BCryptPasswordEncoder with the key “bcrypt”.

Typically, we can use this PasswordEncoderFactories class as follows:

PasswordEncoder passwordEncoder =

    PasswordEncoderFactories.createDelegatingPasswordEncoder();

On the other hand, the implementation method of PasswordEncoderFactories provides a way for us to customize the DelegatingPasswordEncoder according to our own needs, as shown below:

String idForEncode = "bcrypt";

Map encoders = new HashMap<>();

encoders.put(idForEncode, new BCryptPasswordEncoder());

encoders.put("noop", NoOpPasswordEncoder.getInstance());

encoders.put("pbkdf2", new Pbkdf2PasswordEncoder());

encoders.put("scrypt", new SCryptPasswordEncoder());

encoders.put("sha256", new StandardPasswordEncoder());

 

PasswordEncoder passwordEncoder =

    new DelegatingPasswordEncoder(idForEncode, encoders);

Please note that in Spring Security, the standard storage format for passwords is as follows:

{id}encodedPassword

Here, the id is the type of PasswordEncoder, which is the idForEncode mentioned earlier. Assuming the plaintext password is “password”, after being encrypted with BCryptPasswordEncoder, the resulting ciphertext will be a string like this:

$2a$10$dXJ3SW6G7P50lGmMkkmwe.20cQQubK3.HZWzG3YB1tlRy.fqvM/BG

The ciphertext stored in the database should be as follows:

{bcrypt}$2a$10$dXJ3SW6G7P50lGmMkkmwe.20cQQubK3.HZWzG3YB1tlRy.fqvM/BG

The implementation process above can be obtained by checking the encode() method of DelegatingPasswordEncoder:

@Override
public String encode(CharSequence rawPassword) {
    return PREFIX + this.idForEncode + SUFFIX + this.passwordEncoderForEncode.encode(rawPassword);
}

Now let’s look at the matcher() method of DelegatingPasswordEncoder:

@Override
public boolean matches(CharSequence rawPassword, String prefixEncodedPassword) {
    if (rawPassword == null && prefixEncodedPassword == null) {
        return true;
    }

    // Extract the id of the PasswordEncoder
    String id = extractId(prefixEncodedPassword);

    // Get the corresponding PasswordEncoder based on the id of the PasswordEncoder
    PasswordEncoder delegate = this.idToPasswordEncoder.get(id);

    // If the corresponding PasswordEncoder cannot be found, use the default PasswordEncoder for matching
    if (delegate == null) {
        return this.defaultPasswordEncoderForMatches.matches(rawPassword, prefixEncodedPassword);
    }

    // Extract the ciphertext from the stored password string, removing the id
    String encodedPassword = extractEncodedPassword(prefixEncodedPassword);

    // Use the corresponding PasswordEncoder to match the ciphertext
    return delegate.matches(rawPassword, encodedPassword);
}

The above method has a clear process. With that, we have explained the implementation principle of DelegatingPasswordEncoder and further understood the usage process of PasswordEncoder.

Spring Security Encryption Module #

As mentioned at the beginning, when using Spring Security, encryption and decryption techniques are typically used in the user authentication part. However, encryption and decryption techniques are a general infrastructure technology that can be used not only for user authentication but also for any other scenario involving sensitive data processing. Therefore, Spring Security also considers this requirement and provides a dedicated encryption module called the Spring Security Crypto Module (SSCM).

Please note that although PasswordEncoder is also part of this module, this module itself is highly independent, and we can use it separately from the user authentication process.

The core functions of the Spring Security Encryption Module include two parts. The first part is the encryptors, and it is typically used as follows:

BytesEncryptor e = Encryptors.standard(password, salt);

The above method uses the standard 256-bit AES algorithm to encrypt the input password field and returns a BytesEncryptor. At the same time, we see that a salt field representing the salt value is required here, and this salt value can be obtained using another feature of the Spring Security Encryption Module called key generators, as shown below:

String salt = KeyGenerators.string().generateKey();

The above key generator will create an 8-byte key and encode it as a hexadecimal string.

By combining the encryptor and key generator, we can implement a universal encryption and decryption mechanism, as shown below:

String salt = KeyGenerators.string().generateKey(); 
String password = "secret"; 
String valueToEncrypt = "HELLO"; 

BytesEncryptor e = Encryptors.standard(password, salt); 

byte[] encrypted = e.encrypt(valueToEncrypt.getBytes()); 
byte[] decrypted = e.decrypt(encrypted);

In daily development, you can adjust the above code according to your needs and embed it into our system.

Summary and Preview #

For a web application, once user authentication needs to be implemented, encryption of sensitive information such as user passwords becomes necessary. For this purpose, Spring Security provides the PasswordEncoder component specifically for encrypting and decrypting passwords. Spring Security has built-in a set of plug-and-play PasswordEncoder implementations and achieves version compatibility and unified management of various components through proxy mechanisms. This design approach is worth learning and emulating. Of course, as a general security development framework, Spring Security also provides a highly independent encryption module to address daily development needs.

In summary, the content of this course is summarized as follows:

Drawing 3.png

Here’s a question for you to think about: Can you describe the proxy role of DelegatingPasswordEncoder? Feel free to share your thoughts with me in the comments.