When developing a backend application, you must know how to store user information in the database in a secure manner. It doesn’t matter whether you are a freelancer or a multi-billion dollar corporation — you need to assume that somebody may one day break into your database. You are always exposed to the risks of a hacker attack from the outside or a data leak from the inside.

Encryption and hashing principles

One of the ways to protect personal data is encryption. There are two kinds of encryption: symmetric and asymmetric. The major difference between them is in the keys used to encrypt and decrypt data. In this topic, we’ll focus only on the symmetric encryption algorithm. Unlike asymmetric encryption, it uses the same key to encrypt and decrypt data. One of the most widely known and simple examples of symmetric encryption algorithms is Caesar Cipher. You should always remember that everyone with a key can decrypt information. So, for data safety, it’s important to properly store the keys in a secure place and prevent unwanted access. With personal data, be mindful of mandatory requirements for its storage and encryption that may be foreseen by legislation. For instance, storing credit card information falls under the PCI DSS rules, a regulation established by the payment systems applied internationally.

As for the passwords, in general, they should be hashed. Every time a user logs into the system, we will hash their password and compare it with the stored hash in the database. For this reason, in cases where the user forgets the password, they can only reset it, but not retrieve the original password from the system. This ensures that even if the attackers get access to the password hashes list, they can’t use them directly. Nevertheless, there is still the risk of the hashed passwords getting hacked through a brute force or rainbow table attack. To slow down the brute force, you can use a solid hashing algorithm, which makes it too time-consuming for the hackers. As for the rainbow table, adding “salt” to the password before hashing protects against it. By salt here we mean a string that is usually stored in the database along with the hashed password. The idea of the salt is to generate different hashes for the same passwords. For better protection, it is best to use a random and a long enough string as the salt.

Hashing, salting, and encrypting are very complex procedures. Some bugs in the implementation can lead to critical consequences, so it’s better to leave it to security experts. In Spring Security, you can find the Spring Crypto module that includes encryptors, key generators, and password encoders, making it easier to use in your system. In the following sections, we’ll describe each of the elements of this module in more detail.

Adding Spring Crypto to your project

If you are using Spring Boot Security starter, you don’t need to add any additional dependencies to use Spring Crypto: they are already included.

Gradle

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-security:2.7.2'
}        

Maven

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
    <version>2.7.2</version>
</dependency>
		

You can also add Spring Crypto as a separate dependency, even without using the whole Spring Framework:

Gradle

dependencies {
    implementation 'org.springframework.security:spring-security-crypto:5.7.2'
}        

Maven

<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-crypto</artifactId>
    <version>5.7.2</version>
</dependency>

Encryptors and key generators

Spring Crypto has two main interfaces for encryptors:

  • BytesEncryptor
  • TextEncryptor

The first one declares the encryption and decryption of byte[]. The second one will work with String. Any implementation of both interfaces can be constructed with a static method of the Encryptors class.

Declare AesBytesEncryptor as a bean:

@Bean
public BytesEncryptor aesBytesEncryptor() {
    String password = "hackme"; // should be kept in a secure place and not be shared
    String salt = "8560b4f4b3"; // should be hex-encoded with even number of chars
    return Encryptors.standard(password, salt);
}

Below we are using the default BytesEncryptor, which uses 256-bit AES symmetric encryption algorithm. This is one of the most secure algorithms that has been approved by the U.S. government and applied to store top secret information.

How to use the encryptor:

byte[] inputData = {104, 121, 112, 101, 114, 115, 107, 105, 108, 108};
byte[] encryptedData = bytesEncryptor.encrypt(inputData);
byte[] decryptedData = bytesEncryptor.decrypt(encryptedData);

System.out.println("Input data: " + new String(inputData));
System.out.println("Encrypted data: " + new String(encryptedData));
System.out.println("Decrypted data: " + new String(decryptedData));

The output will be:

Input data: codeyz.com
Encrypted data: �*�j�S�*)� ��^��#�|��� d-%
Decrypted data: codeyz.com

Most of the time, data that needs to be encrypted can be represented as String. In that case, it’s better to use TextEncryptor. It has two implementations:

  • NoOpTextEncryptor
  • HexEncodingTextEncryptor

NoOp stands for “No Operation”, so this encryptor does nothing and doesn’t really interest us.

HexEncodingTextEncryptor uses the previously discussed BytesEncryptor under the hood. The encrypted data will be hex-encoded, so it’s easier to store it in the file system or database.

Define HexEncodingTextEncryptor:

@Bean
public TextEncryptor hexEncodingTextEncryptor() {
    String password = "hackme"; // should be kept in a secure place and not be shared
    String salt = "8560b4f4b3"; // should be hex-encoded with even number of chars
    return Encryptors.text(password, salt);
}

Use it to encode the text:

String inputData = "codeyz.com";
String encryptedData = textEncryptor.encrypt(inputData);
String decryptedData = textEncryptor.decrypt(encryptedData);

System.out.println("Input data: " + inputData);
System.out.println("Encrypted data: " + encryptedData);
System.out.println("Decrypted data: " + decryptedData);

Here is the output:

Input data: codeyz.com
Encrypted data: ec2f77cff5a92723a9a101193f33937068802fb0dda2d70df440dc35c7629d7a
Decrypted data: codeyz.com

As you can see in the examples above, we hard-coded our “salt” value in the encryptor bean. However, you can also use mechanisms provided by Spring, which is even better because then you’ll use a randomly generated “salt”:

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

This line will generate a hex-encoded key that is 8 bytes in length:

861fddff2d08c730

Passwords encoders

Passwords are hashed by the use of PasswordEncoder. This interface declares methods to encode input and match hash with raw password. Let’s see how to use the most simple encoder implementation NoOpPasswordEncoder, which, as you can understand from its name, leaves the password as it is.

PasswordEncoder noOpEncoder = NoOpPasswordEncoder.getInstance();

String rawPassword = "hackme";
String encodedPassword = noOpEncoder.encode("hackme");
boolean isMatched = noOpEncoder.matches(rawPassword, encodedPassword);

System.out.println("Encoded password: " + encodedPassword);
System.out.println("Match: " + isMatched);

Here is the output:

Encoded password: hackme
Match: true

This encoder is not secure, which means it is deprecated. It is provided in the library for legacy and testing purposes only. These are the recommended implementations:

  • BCryptPasswordEncoder
  • PbkdfPasswordEncoder
  • SCryptPasswordEncoder

Take a closer look at BCryptPasswordEncoder:

int strength = 7;
PasswordEncoder bCryptEncoder = new BCryptPasswordEncoder(strength);

String rawPassword = "hackme";
String firstEncodedPassword = bCryptEncoder.encode(rawPassword);
String secondEncodedPassword = bCryptEncoder.encode(rawPassword);

System.out.println("First encoded password: " + firstEncodedPassword);
System.out.println("Second encoded password: " + secondEncodedPassword);

As you can see, we are passing the parameter to the encoder constructor. This is one of the “strengths” of the bCrypt algorithm: log rounds to use. In this way, the algorithm allows the system able to adapt to more powerful computers, so more work has to be done to calculate the hash. Try to use a higher number and see how it will affect the performance.

And the output will be the following:

First encoded password: $2a$07$Q/XxLrLPHniRGl9PNSLiSuPGcDkbv54U3.DAEQXRpSd5qKXpCk2V2
Second encoded password: $2a$07$.8cNtKbeAqDrxsPAquxRRuSYpg6RKCkqxoW70Oxxht7yKLPUcXQye

We passed the same password to both encode calls but got completely different results. This is because the bCrypt algorithm implementation in Spring will manage “salt” for you.

There is one more implementation left, namely DelegatingPasswordEncoder. It acts as a facade to all password encoding algorithms and makes it possible to select an algorithm at runtime. The algorithm that was used is stored with a password in the following format: {id}encodedPassword. Below you can find examples of passwords encoded with DelegatingPasswordEncoder.

Conclusion

Now you know about encryptors, key generators, and passwords that compose the Spring Crypto module of the Spring Security library and how to use them.

Remember, if you store users’ passwords somewhere, they should be hashed. Which data should be encrypted is more difficult to decide as there is no straight answer. A good idea is to perform a risk assessment about possible attack threats and check the regulatory requirements.

A variety of complex algorithms has already been implemented for you by Spring developers. You can choose the one that suits your needs most and provides better security to keep valuable information safe.

Leave a Reply

Your email address will not be published.