How to encrypt and decrypt JSON properties with JPA

Imagine having a tool that can automatically detect JPA and Hibernate performance issues. Wouldn’t that be just awesome?

Well, Hypersistence Optimizer is that tool! And it works with Spring Boot, Spring Framework, Jakarta EE, Java EE, Quarkus, or Play Framework.

So, enjoy spending your time on the things you love rather than fixing performance issues in your production system on a Saturday night!

Introduction

In this article, we are going to see how we can encrypt and decrypt JSON properties when using JPA and Hibernate.

While encrypting the entire column value is very straightforward, when it comes to JSON columns, we need to preserve the JSON object structure while only encrypting the JSON property values.

Domain Model

Let’s assume that our application defines a User entity that encapsulates all the user-sensitive information in a UserDetails object:

User and UserDetails JPA entities

The User entity is mapped to the users database table, and the UserDetails object is saved in a JSON column:

The users table to encrypt decrypt JSON column

Since the UserDetails contains user-specific information, we want to encrypt it prior to storing it in the database and decrypt it after loading it.

How to encrypt and decrypt JSON properties with JPA and Hibernate

The UserDetails is a simple POJO class that looks as follows:

public class UserDetails {

    private String firstName;

    private String lastName;

    private String emailAddress;

    //Getters and setters omitted for brevity
}

The User entity is going to be mapped as follows.

For Hibernate 6, the mapping will look as follows:

@Entity
@Table(name = "users")
@DynamicUpdate
public class User {

    @Id
    private Long id;

    private String username;

    @Type(JsonType.class)
    @Column(columnDefinition = "json")
    private UserDetails details;

    //Getters and setters omitted for brevity

    @PrePersist
    @PreUpdate
    private void encryptFields() {
        if (details != null) {
            if (details.getFirstName() != null) {
                details.setFirstName(
                    CryptoUtils.encrypt(details.getFirstName())
                );
            }
            if (details.getLastName() != null) {
                details.setLastName(
                    CryptoUtils.encrypt(details.getLastName())
                );
            }
            if (details.getEmailAddress() != null) {
                details.setEmailAddress(
                    CryptoUtils.encrypt(details.getEmailAddress())
                );
            }
        }
    }

    @PostLoad
    private void decryptFields() {
        if (details != null) {
            if (details.getFirstName() != null) {
                details.setFirstName(
                    CryptoUtils.decrypt(details.getFirstName())
                );
            }
            if (details.getLastName() != null) {
                details.setLastName(
                    CryptoUtils.decrypt(details.getLastName())
                );
            }
            if (details.getEmailAddress() != null) {
                details.setEmailAddress(
                    CryptoUtils.decrypt(details.getEmailAddress())
                );
            }
        }
    }
}

And for Hibernate 5, like this:

@Entity
@Table(name = "users")
@DynamicUpdate
@TypeDef(typeClass = JsonType.class, defaultForType = UserDetails.class)
public class User {

    @Id
    private Long id;

    private String username;

    @Column(columnDefinition = "json")
    private UserDetails details;

    //Getters and setters omitted for brevity

    @PrePersist
    @PreUpdate
    private void encryptFields() {
        if (details != null) {
            if (details.getFirstName() != null) {
                details.setFirstName(
                    CryptoUtils.encrypt(details.getFirstName())
                );
            }
            if (details.getLastName() != null) {
                details.setLastName(
                    CryptoUtils.encrypt(details.getLastName())
                );
            }
            if (details.getEmailAddress() != null) {
                details.setEmailAddress(
                    CryptoUtils.encrypt(details.getEmailAddress())
                );
            }
        }
    }

    @PostLoad
    private void decryptFields() {
        if (details != null) {
            if (details.getFirstName() != null) {
                details.setFirstName(
                    CryptoUtils.decrypt(details.getFirstName())
                );
            }
            if (details.getLastName() != null) {
                details.setLastName(
                    CryptoUtils.decrypt(details.getLastName())
                );
            }
            if (details.getEmailAddress() != null) {
                details.setEmailAddress(
                    CryptoUtils.decrypt(details.getEmailAddress())
                );
            }
        }
    }
}

The @DynamicUpdate annotation is used because we want Hibernate to include only the modified columns when generating an UPDATE statement. For more details about the @DynamicUpdate annotation, check out this article.

The @TypeDef annotation is used for Hibernate 5 only, and it instructs Hibernate to use the JsonType provided by the Hypersistence Utils project when persisting and fetching entity attributes of the UserDetails type.

The encryptFields method is annotated with the JPA @PrePersist and @PreUpdate annotations, so the JPA provider will call this method prior to persisting or updating the entity. Therefore, we are going to use the encryptFields method to encrypt the attribute values of the UserDetails object.

The decryptFields method is annotated with the JPA @PostLoad annotation, so the JPA provider is going to call this method after fetching the entity. Therefore, we are going to use the decryptFields method to decrypt the attribute values of the UserDetails object.

The CryptoUtils class is located in my High-Performance Java Persistence GitHub repository, and for brevity sake, it’s been omitted.

For more details about the @PrePersist, @PreUpdate, and @PostLoad JPA annotations, check out this article as well.

Testing time

When persisting the following User entity:

entityManager.persist(
    new User()
        .setId(1L)
        .setUsername("vladmihalcea")
        .setDetails(
            new UserDetails()
            .setFirstName("Vlad")
            .setLastName("Mihalcea")
            .setEmailAddress("info@vladmihalcea.com")
        )
);

Hibernate generates the following SQL INSERT statement:

INSERT INTO users (
    details, 
    username, 
    id
) 
VALUES (
    {
        "firstName":"3Pj42hikNEQ5Z3gQplc2AQ==",
        "lastName":"xTC5Ef4MFEhU4/K7a7+WHw==",
        "emailAddress":"6IuTqZ4e9N80vvutCztnddjNpvuNe/BGn1MrAck3sic="
    }, 
    vladmihalcea, 
    1
)

Notice that only the JSON property values have been encrypted. The details column value is still a valid JSON object. If we encrypted the entre JSON column value, the DB would throw a constraint violation since the provided encrypted string value would not be a valid JSON object.

When loading the User entity, we can see that the UserDetails properties are properly decrypted:

User user = entityManager.find(User.class,1L);

UserDetails userDetails = user.getDetails();

assertEquals("Vlad", userDetails.getFirstName());
assertEquals("Mihalcea", userDetails.getLastName());
assertEquals("info@vladmihalcea.com", userDetails.getEmailAddress());

When updating a UserDetails property:

User user = entityManager.find(User.class, 1L);

user.getDetails().setEmailAddress("noreply@vladmihalcea.com");

We can see that the UPDATE statement will contain the new details column value with the emailAddress property value containing the new encrypted email value:

UPDATE users 
SET 
    details = {
        "firstName":"3Pj42hikNEQ5Z3gQplc2AQ==",
        "lastName":"xTC5Ef4MFEhU4/K7a7+WHw==",
        "emailAddress":"JBBe6+rKdNjWdp47rFOy29l1X6vnY3L3R5OhCZGaF74="
    }
WHERE 
    id = 1

Awesome, right?

I'm running an online workshop on the 20-21 and 23-24 of November about High-Performance Java Persistence.

If you enjoyed this article, I bet you are going to love my Book and Video Courses as well.

Conclusion

JPA makes it very easy to encrypt and decrypt JSON properties due to its entity listener methods. And, if you’re using Hibernate, you can benefit from the Hypersistence Utils project to map JSON columns, no matter if you’re using Oracle, SQL Server, PostgreSQL or MySQL.

Transactions and Concurrency Control eBook

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.