Post Thumbnail

How to Automatically Encrypt Sensitive Information in Database Columns

1. Overview

In building web applications, we sometimes need to store sensitive information in a database. This can either be an API key, credit card details, or client secret keys.

Storing such data in plain text poses significant security risks, making encryption essential.

In this article, we are going to learn how to automatically encrypt and decrypt sensitive data, stored in a database, in a SpringBoot application using AES and JPA attribute converter.

2. Project Setup

The project we will be creating is a Spring Boot application that stores sensitive client secret keys in a database table. It will use AES (Advance Encryption Standard) to encrypt the data.

Data encryption will happen automatically while saving the data and decryption will happen when reading the data, back from the database.

The application connects to a PostgreSQL database, and uses Flyway to create the required database table.

The complete source code will be available at the end of this tutorial.

3. Automatic Database Column Encryption and Decryption

Let’s start by creating the UserSecretKey model, that maps to the user_secret_keys table containing the secret key column.

Listing 3.1 UserSecretKey.java

 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
@Table(name = "user_secret_keys")
@Entity
@SQLRestriction("abolished_at IS NULL")
public class UserSecretKey {

    @Id
    @SequenceGenerator(name = "user_secret_keys_id_seq", sequenceName = "user_secret_keys_id_seq", allocationSize = 1)
    @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "user_secret_keys_id_seq")
    private long id;

    @OneToOne
    private User user;
    
    private String hmacSecretKey;

    private String clientId;

    @CreationTimestamp
    private LocalDateTime createdAt;

    @UpdateTimestamp
    private LocalDateTime updatedAt;

    private LocalDateTime abolishedAt;

    //getters and setters omitted
}

The sensitive column we want to encrypt is hmacSecretKey.

Let’s create helper methods for encrypting and decrypting any data. These are reusable methods that can be invoked whenever we want to encrypt and decrypt data in AES.

Listing 3.2 CryptoHelper.java

 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
public static String encryptDataAES(String plainData, byte[] key) throws Exception {

    SecretKey secretKey = new SecretKeySpec(key, "AES");

    //build the initialization vector
    byte[] iv = new byte[CRYPTO_IV_LENGTH];
    SecureRandom secureRandom = new SecureRandom();
    secureRandom.nextBytes(iv);

    GCMParameterSpec parameterSpec = new GCMParameterSpec(CRYPTO_AUTH_TAG_LENGTH, iv); //128-bit authentication tag length

    Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
    cipher.init(Cipher.ENCRYPT_MODE, secretKey, parameterSpec);

    byte[] cipherText = cipher.doFinal(plainData.getBytes());

    ByteBuffer byteBuffer = ByteBuffer.allocate(iv.length + cipherText.length);
    byteBuffer.put(iv);
    byteBuffer.put(cipherText);


    //the first 12 bytes are the IV where others are the cipher message + authentication tag
    byte[] cipherMessage = byteBuffer.array();
    return Base64.getEncoder().encodeToString(cipherMessage);

}


public static String decryptDataAES(String encryptedDataInBase64, byte[] key) throws Exception {

    SecretKey secretKey = new SecretKeySpec(key, "AES");

    Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");

    byte[] encryptedDataBytes = Base64.getDecoder().decode(encryptedDataInBase64.getBytes());

    //remember we stored the IV as the first 12 bytes while encrypting?
    byte[] iv = Arrays.copyOfRange(encryptedDataBytes, 0, CRYPTO_IV_LENGTH);

    GCMParameterSpec parameterSpec = new GCMParameterSpec(CRYPTO_AUTH_TAG_LENGTH, iv); //128-bit authentication tag length
    cipher.init(Cipher.DECRYPT_MODE, secretKey, parameterSpec);

    //use everything from 12 bytes on as ciphertext
    byte [] cipherBytes = Arrays.copyOfRange(encryptedDataBytes, CRYPTO_IV_LENGTH, encryptedDataBytes.length);

    byte[] plainText = cipher.doFinal(cipherBytes);

    return new String(plainText);

}

The above functions will help us to encrypt and decrypt any data using AES. We discussed them extensively in this article.

We will create two more functions, to specifically handle scenarios where the database column is NULL and other exceptions.

Listing 3.3 CryptoHelper.java

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
public String encryptDeviceCredential(String plainData) {
    if(Objects.isNull(plainData) || plainData.isEmpty()) return plainData;
    return Try.of(() -> encryptDataAES(plainData, hmacSecretKeyEncryptionKey.getBytes()))
            .get();
}


public String decryptDeviceCredential(String cipherText) {
    if(Objects.isNull(cipherText) || cipherText.isEmpty()) return cipherText;
    return Try.of( () -> decryptDataAES(cipherText, hmacSecretKeyEncryptionKey.getBytes()))
            .get();
}

The next step is to create a custom implementation of the jakarta.persistence.AttributeConverter interface.

Listing 3.4 HmacSecretKeyAttributeConverter.java

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
public class HmacSecretKeyAttributeConverter
        implements AttributeConverter<String, String> {

    @Override
    public String convertToDatabaseColumn(String attribute) {
        CryptoHelper cryptoHelper = SpringContext.getBean(CryptoHelper.class);
        return cryptoHelper.encryptDeviceCredential(attribute);
    }

    @Override
    public String convertToEntityAttribute(String dbData) {
        CryptoHelper cryptoHelper = SpringContext.getBean(CryptoHelper.class);
        return cryptoHelper.decryptDeviceCredential(dbData);
    }

}

In Listing 3.4 above, we use SpringContext to get an instance of the CryptoHelper class from the Spring Context.

This is because HmacSecretKeyAttributeConverter.java will be instantiated automatically by JPA/Hibernate, when the application is booting up.

This leaves us with the options to either make the functions cryptoHelper.encryptDeviceCredential and cryptoHelper.decryptDeviceCredential static or use the SpringContext.

The SpringContext helper class gets uses org.springframework.context.ApplicationContext to get an instance of CryptoHelper class. This is possible because CryptoHelper is a Spring managed bean. It is a Spring managed bean because it is annotated with @Service.

Finally, the last part of the process is to apply the custom attribute converter to the database column we want.

Listing 3.5 UserSecretKey.java

 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
@Table(name = "user_secret_keys")
@Entity
@SQLRestriction("abolished_at IS NULL")
public class UserSecretKey {

    @Id
    @SequenceGenerator(name = "user_secret_keys_id_seq", sequenceName = "user_secret_keys_id_seq", allocationSize = 1)
    @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "user_secret_keys_id_seq")
    private long id;

    @OneToOne
    private User user;

    @Convert(converter = HmacSecretKeyAttributeConverter.class)
    private String hmacSecretKey;

    private String clientId;

    @CreationTimestamp
    private LocalDateTime createdAt;

    @UpdateTimestamp
    private LocalDateTime updatedAt;

    private LocalDateTime abolishedAt;
    
    //getters and setters omitted
}

The major changes are in lines 14 and 15 of UserSecretKey.java above. We added the @Convert(converter = HmacSecretKeyAttributeConverter.class) on the hmacSecretKey column.

This way, anytime we save a new UserSecretKey record, via its repository, the hmacSecretKey column will be automatically encrypted in the database.

Also, when we read the UserSecretKey, it will be decrypted automatically, making the plain text value available in memory for other processing.

4. Integration Testing

Let’s validate the setup by creating an integration test that will save a new UserSecretKey record with a plain text secret.

We will then read the record, using JdbcTemplate.

Using JdbcTemplate means there will be no automatic conversion, and we will be able to confirm the secret is not stored in plain text.

We will also read the record using the UserSecretKeyRepository class and assert the automatic conversion works as expected.

Listing 4.1 UserSecretKeyRepositoryUnitTest.java

 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
@Test
void givenPlainTextSecret_whenSaveUserSecretKey_thenEncryptAndSave() {

    String plainTextSecret = "area57PassKey";
    String clientId = "demoClient";

    //create a new record
    UserSecretKey userSecretKey = new UserSecretKey();
    userSecretKey.setHmacSecretKey(plainTextSecret);
    userSecretKey.setClientId(clientId);

    //save the record
    userSecretKeyRepository.save(userSecretKey);

    //confirm the secret is encrypted
    String sql = "SELECT hmac_secret_key FROM user_secret_keys WHERE client_id='%s'"
            .formatted(clientId);
    String secretKeyInDB = jdbcTemplate.queryForObject(sql, String.class);

    //confirm they're not the same thing because secretKeyInDB is
    // the encrypted format.
    Assertions.assertNotEquals(plainTextSecret, secretKeyInDB);


    //read the data via repository
    UserSecretKey loadedUserSecretKey = userSecretKeyRepository
            .findFirstByClientId(clientId);

    //this will be true because automatic decryption has happened
    Assertions.assertEquals(plainTextSecret, loadedUserSecretKey.getHmacSecretKey());


}

5. Conclusion

We have seen how to use attribute converter to automatically encrypt and decrypt data stored in a database. Beyond this use case, the attribute converter can also be used for other things like converting from one data type to another and many others.

Although we used AES encryption in this article, the technique can be easily adapted for other encryption algorithms.

As we can see in the test in section 4, the automatic encryption and decryption is only applied when we use a JPA Repository instance to persist and read the data we need.

The complete source code is available on GitHub.

If you find this tutorial helpful, don’t forget to check out our other Spring Boot security articles. Share your thoughts or ask questions in the comments below!

Happy Coding

Seun Matt

Results-driven Engineer, dedicated to building elite teams that consistently achieve business objectives and drive profitability. With over 8 years of experience, spannning different facets of the FinTech space; including digital lending, consumer payment, collections and payment gateway using Java/Spring Boot technologies, PHP and Ruby on Rails