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:
The User
entity is mapped to the users
database table, and the UserDetails
object is saved in a 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?
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.
