In 2018, it’s mandatory to think about security for every application which stores personal data. When it comes to this topic, you can’t be 100% sure that the application has no vulnerabilities thus it’s wise to make the data harder to read in case of a data leak which practically means storing sensitive information in an encrypted form, usually in the database.
As Hibernate has quite a big share in the industry for reading and writing data, I’ll show how to employ it’s capabilities to make encryption easier and cleaner using only annotations on the selected entity attributes.
The encryption
For demonstration purposes, I’m not going to use real encryption but just Base64 encoding and decoding. The encryption and decryption implementation is a very separated logic from all the other parts of the application, it might involve invoking some HTTP API to do the encryption/decryption but it can be as simple as a simple method call.
I’ll use the following implementation for encryption:
@Component public class Encrypter { public String encrypt(String value) { return Base64.getEncoder().encodeToString(value.getBytes(StandardCharsets.UTF_8)); } }
And the following for decryption:
@Component public class Decrypter { public String decrypt(String value) { return new String(Base64.getDecoder().decode(value.getBytes(StandardCharsets.UTF_8)), StandardCharsets.UTF_8); } }
Wiring encryption into entities
Here comes the tricky part. How to handle encryption when saving or loading an entity? The expectation would be when saving a new or updating or reading an existing entity, the value is in a unencrypted format within the application but in the database, it’s stored encrypted.
The options are the following in case of JPA:
- Using an AttributeConverter
- I don’t like this option as it’s not about converting types and the main purpose of this interface would be to create a mapping between the database and the application representation.
- Using an EntityListener
- This seems to be a way too verbose solution. The @EntityListener(EncryptionListener.class) must be put on the class however encryption must be a default for all the entities in the application.
- Using a Hibernate Interceptor
- This could be an option but it’s not that easy to register an Interceptor within Spring Boot, unless you are manually doing the SessionFactory registration.
- Manually handling the encryption and decryption before saving and reading an entity. This has the downside that the business logic will be mixed up with the encryption and it could be simply forgotten therefore having some sensitive data in an unencrypted form.
The first 2 options, using an AttributeConverter, EntityListener has a big downside. You cannot easily access the Spring context as the instances of the classes will not be managed by Spring which means you cannot get any dependency autowired into those instances. However, there is a solution for this problem by saving the ApplicationContext into a static store which can be accessed by the AttributeConverter or EntityListener but obviously this is a pretty bad solution.
The 3rd option would be a good one but as I mentioned, hard to register it in Spring Boot. Manually doing the encryption/decryption could be simply forgotten which is the main problem with it in my opinion.
There is one more option, using EventListeners. Hibernate defines a couple of different events which happens during an entity’s lifecycle like before updating an entity, before persisting it and so on. Implementing encryption will require 3 events to be caught, before persisting an entity, before updating an entity and after loading the entity from the database.
For different lifecycle events, Hibernate defines different interfaces.
All we have to do is to implement these interfaces and register those implementations through the EntityManagerFactory which is automatically created by Spring Boot.
The solution
For the sake of simplicity, I’ll use an entity with 2 fields, one for the id and one which represents personal data and needs to be encrypted. This entity will be representing a Phone number.
@Entity public class Phone { @Id private UUID id; @Column(name = "phone_number") @Encrypted private String phoneNumber; protected Phone() { } public Phone(String phoneNumber) { this.id = UUID.randomUUID(); this.phoneNumber = phoneNumber; } // getters & setters omitted }
The id column will be a UUID, but it could be any other type and the phone number will be stored in the phoneNumber attribute. The latter attribute is special because it has a custom annotation, @Encrypted . This annotation will be used in the event listeners to determine which attributes needs to be encrypted and decrypted. The annotation looks a very simple one:
@Target(ElementType.FIELD) @Retention(RetentionPolicy.RUNTIME) public @interface Encrypted { }
Let’s create the event listener. One class will implement all the 3 listeners which is needed for the encryption. Notice that it is annotated as @Component and by the consequence of that, Spring will manage the bean instance and autowiring is now possible.
@Component public class EncryptionListener implements PreInsertEventListener, PreUpdateEventListener, PostLoadEventListener { @Autowired private FieldEncrypter fieldEncrypter; @Autowired private FieldDecrypter fieldDecrypter; @Override public void onPostLoad(PostLoadEvent event) { fieldDecrypter.decrypt(event.getEntity()); } @Override public boolean onPreInsert(PreInsertEvent event) { Object[] state = event.getState(); String[] propertyNames = event.getPersister().getPropertyNames(); Object entity = event.getEntity(); fieldEncrypter.encrypt(state, propertyNames, entity); return false; } @Override public boolean onPreUpdate(PreUpdateEvent event) { Object[] state = event.getState(); String[] propertyNames = event.getPersister().getPropertyNames(); Object entity = event.getEntity(); fieldEncrypter.encrypt(state, propertyNames, entity); return false; } }
There are 2 dependencies, FieldEncrypter and FieldDecrypter , both will be described a bit later. Have a look at the implementation of the methods coming from the interfaces. onPostLoad is called when the entity instance is filled up with the values from the database but before giving back control to the application. onPreInsert is called when persist is called but before executing the INSERT statement. onPreUpdate is the same as onPreInsert , but it’s called before an UPDATE statement is executed.
The latter 2 methods are passing a PreInsert and PreUpdate event as a parameter. This has a reference to the actual entity being worked on but it’s a bit tricky as any change you made to those entity instances will be lost in the SQL statements. Instead of modifying the entity, there is a state parameter passed along which is the store of the data and represented as an Object array. The ordering of this array will match the ordering of the array returned by org.hibernate.persister.entity.EntityPersister#getPropertyNames . The trick is to manipulate this state array instead of the entity so the change will be propagated to the database.
Now check out how the encryption is done with the FieldEncrypter .
@Component public class FieldEncrypter { @Autowired private Encrypter encrypter; public void encrypt(Object[] state, String[] propertyNames, Object entity) { ReflectionUtils.doWithFields(entity.getClass(), field -> encryptField(field, state, propertyNames), EncryptionUtils::isFieldEncrypted); } private void encryptField(Field field, Object[] state, String[] propertyNames) { int propertyIndex = EncryptionUtils.getPropertyIndex(field.getName(), propertyNames); Object currentValue = state[propertyIndex]; if (!(currentValue instanceof String)) { throw new IllegalStateException("Encrypted annotation was used on a non-String field"); } state[propertyIndex] = encrypter.encrypt(currentValue.toString()); } }
Nothing fancy, it takes all the fields which are annotated with @Encrypted and then gets the field value from the state parameter, then applies the encryption (which is Base64 for the moment) and writes it back to the state array.
EncryptionUtils is implemented as following:
public abstract class EncryptionUtils { public static boolean isFieldEncrypted(Field field) { return AnnotationUtils.findAnnotation(field, Encrypted.class) != null; } public static int getPropertyIndex(String name, String[] properties) { for (int i = 0; i < properties.length; i++) { if (name.equals(properties[i])) { return i; } } throw new IllegalArgumentException("No property was found for name " + name); } }
Now comes the FieldDecrypter :
@Component public class FieldDecrypter { @Autowired private Decrypter decrypter; public void decrypt(Object entity) { ReflectionUtils.doWithFields(entity.getClass(), field -> decryptField(field, entity), EncryptionUtils::isFieldEncrypted); } private void decryptField(Field field, Object entity) { field.setAccessible(true); Object value = ReflectionUtils.getField(field, entity); if (!(value instanceof String)) { throw new IllegalStateException("Encrypted annotation was used on a non-String field"); } ReflectionUtils.setField(field, entity, decrypter.decrypt(value.toString())); } }
Similarly, it takes the @Encrypted fields and sets those to accessible for further using it through reflection. Then the value of the field is taken, decrypted and set back to the field.
This was the encryption implementation but still some configuration is needed which is registering the event listener in Hibernate. Here I’ll utilize a BeanPostProcessor which will use the EntityManagerFactory to register the listener.
@Component public class EncryptionBeanPostProcessor implements BeanPostProcessor { private static final Logger logger = LoggerFactory.getLogger(EncryptionBeanPostProcessor.class); @Autowired private EncryptionListener encryptionListener; @Override public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException { return bean; } @Override public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { if (bean instanceof EntityManagerFactory) { HibernateEntityManagerFactory hibernateEntityManagerFactory = (HibernateEntityManagerFactory) bean; SessionFactoryImpl sessionFactoryImpl = (SessionFactoryImpl) hibernateEntityManagerFactory.getSessionFactory(); EventListenerRegistry registry = sessionFactoryImpl.getServiceRegistry().getService(EventListenerRegistry.class); registry.appendListeners(EventType.POST_LOAD, encryptionListener); registry.appendListeners(EventType.PRE_INSERT, encryptionListener); registry.appendListeners(EventType.PRE_UPDATE, encryptionListener); logger.info("Encryption has been successfully set up"); } return bean; } }
UPDATE: The implementation showed above was suffering from a performance issue because as soon as the entity was decrypted after loading it into the persistence context, Hibernate will think that “oops, someone changed the state of the entity, let’s write it back to the database”, meaning that at the end of the transaction, there was an unnecessary UPDATE statement executed.
Instead of using the PostLoadListener , one can utilize the PreLoadListener which kicks in right before the actual entity loading and let’s you operate on “state” level. Modifying this internal state will result in the proper behavior and implementation looks similar to the encryption logic.
The updated code can be found on GitHub.
Testing time
Let’s see encryption at work. I’ll use a custom class which helps with the transaction handling.
@Component public class TransactionalRunner { @PersistenceContext private EntityManager em; @Transactional(propagation = Propagation.REQUIRES_NEW) public void doInTransaction(final Consumer<EntityManager> c) { c.accept(em); } @Transactional(propagation = Propagation.REQUIRES_NEW) public <T> T doInTransaction(final Function<EntityManager, T> f) { return f.apply(em); } }
Two bigger tests will be created, one for testing that persisting works and one for updating. Implicitly the reading will be tested in both cases.
@Test public void testInsertionWorks() { String expectedPhoneNumber = "00361234567"; // Persisting a phone entity through JPA, this should encrypt the phone number column UUID phoneId = txRunner.doInTransaction(em -> { Phone newPhone = new Phone(expectedPhoneNumber); em.persist(newPhone); return newPhone.getId(); }); // Checks if the database has the phone number value in an encrypted form txRunner.doInTransaction(em -> { Query query = em.createNativeQuery("SELECT phone_number FROM Phone where id = :phoneId"); query.setParameter("phoneId", phoneId); String nativePhoneNumber = (String) query.getSingleResult(); assertThat(nativePhoneNumber).isNotEqualTo(expectedPhoneNumber); }); // Checks if the decryption happened automatically when getting the row through JPA txRunner.doInTransaction(em -> { Phone phone = em.find(Phone.class, phoneId); assertThat(phone.getPhoneNumber()).isEqualTo(expectedPhoneNumber); }); }
The first one tests persisting encryption by creating a JPA entity instance, and calling persist.. Then reading the database using native SQL to verify that the data is really in an encrypted form and last but not least, reading the entity back from the database through JPA and verifying that it’s decrypted.
@Test public void testUpdateWorks() { String oldPhoneNumber = "0987654321"; String expectedPhoneNumber = "00361234567"; // Persisting a phone entity through JPA, this should encrypt the phone number column UUID phoneId = txRunner.doInTransaction(em -> { Phone newPhone = new Phone(oldPhoneNumber); em.persist(newPhone); return newPhone.getId(); }); // Checks if the database has the phone number value in an encrypted form txRunner.doInTransaction(em -> { Query query = em.createNativeQuery("SELECT phone_number FROM Phone where id = :phoneId"); query.setParameter("phoneId", phoneId); String nativePhoneNumber = (String) query.getSingleResult(); assertThat(nativePhoneNumber).isNotEqualTo(oldPhoneNumber); }); // Update the phone number txRunner.doInTransaction(em -> { Phone phone = em.find(Phone.class, phoneId); phone.setPhoneNumber(expectedPhoneNumber); }); // Checks if the database has the phone number value in an encrypted form txRunner.doInTransaction(em -> { Query query = em.createNativeQuery("SELECT phone_number FROM Phone where id = :phoneId"); query.setParameter("phoneId", phoneId); String nativePhoneNumber = (String) query.getSingleResult(); assertThat(nativePhoneNumber).isNotEqualTo(expectedPhoneNumber); }); // Checks if the decryption happened automatically when getting the row through JPA txRunner.doInTransaction(em -> { Phone phone = em.find(Phone.class, phoneId); assertThat(phone.getPhoneNumber()).isEqualTo(expectedPhoneNumber); }); }
The second test does almost the same but there is an intermediate step to update the data.
At the end, both of the test cases will be green which means encryption is working fine.
Summary
In this article, we’ve seen how to create a custom annotation to encrypt JPA entity attributes in the database. There was one more tiny trick to define the event listener as a Spring bean allowing to use dependency injection for separating the components of the encryption logic.
The full code can be found on GitHub.
If you liked the article, share it and let me know what you think. For more interesting topics, follow me on Twitter.
Tried Hibernate’s @ColumnTransformer a few weeks ago, but having H2 for testing was an issue when using PostgreSQL pgcrypto and there was no time to try to make it work, especially as we might just need it in the future. As an annoying coincidence, both H2 and Postgre have “encrypt” and “decrypt” methods with exactly 3 parameters of String and byte array, but in different order :). Have implemented a good encryption and decryption method for now (mainly from stackoverflow), but will probably use it like you described if we need to use it more extensively.
Hi Attila. Awesome. Let me know how the implementation went.
I wanted to take your example and run it against PostgreSQL. I’ve updated the application.yml to contain the following, but I get o.h.engine.jdbc.spi.SqlExceptionHelper : ERROR: relation “phone” does not exist when I run the test. I’m guessing that unit tests don’t instruct hibernate to create missing database objects?
spring:
jpa:
properties:
hibernate:
ddl-auto: update
show_sql: true
format_sql: true
dialect: org.hibernate.dialect.PostgreSQLDialect
current_session_context_class: org.springframework.orm.hibernate5.SpringSessionContext
temp:
use_jdbc_metadata_defaults: false
datasource:
driver-class-name: org.postgresql.Driver
url: jdbc:postgresql://localhost:5432/testdb
username: testdbuser
password: letmein
Hi Joe. Yes. The configuration is set for “update” mode only. Set the ddl-auto property to create or create-drop and that will instruct Hibernate to create the tables. Let me know how it goes.
Nice article. You changed in github PostLoadEventListener to PreLoadEventListener but on website you have still old code.
Hi Michal. Thanks. Let me fix it!
Michal, I just missed the fact that I’ve made an update already on the post that it was switched off. Thanks though.
I really like this approach, and it is working OK for me now, but I had to Use an @PostConstruct bean instead of the BeanPostProcessor to accomodate a more recent hibernate version. I found this link helpful: https://www.javacodegeeks.com/2016/07/spring-managed-hibernate-event-listeners.html
LMK if you want me to post my modified bean to your github project.
Hi Chris. Strange. The BeanPostProcessor actually has the same functionality as a method with the @PostConstruct annotation.
Is there a reason why the encryption/decryption logic could not be implemented in the getter/setter methods for the entity? I suspect there is, but I haven’t yet found any conclusive information to confirm this.
Hi Tom. For sure you can. In that case you just need to make sure to use getter/setter accessors instead of the field ones.
The problem here is that you can only map String -> String, so every field that needs to be encrypted must be of type String (you modify the state of an entity and don’t change the type of a column). Using an AttributeConverter you can basically map anything to a String and vice versa, so your domain entities don’t need to use Strings for, e.g., dates, numbers etc.
Hi Matthias. You are right. As a matter of fact its a balance game. The option with AttributeConverter was mentioned in the article. It is a possibility, if your use-case fits that better, go for it.
Hello, Arnold, thank you for the great solution!
I found one issue, it breaks when deals with null values (trying to decrypt);
Hi Rostislav. Sure, that is a possibility. I’m sure adding a few null-checks here and there will solve the problem. Let me know how it went.
awesome write up!!!! Was new to Spring + JPA, this was the exact type of read up I was looking for when investigating client side encryption.
Hey Arnold, great solution to keep it out of application code! I was wondering how to solve embedded encrypted fields, any thoughts?
Hey Aaron. I haven’t worked with embedded fields for a long time now. Have you tried the solution? I think on this low-level the state array should contain the unwrapped fields. If it doesn’t then probably another hook would be an option, I’m just not sure which one on the top of my head.
This is the perfect article. thank you.
I know it’s old, but I can’t think of a better way to do it.
I tried to apply it to my latest project and found it works fine up to spring boot 2.x.
However, since 3.x, I am facing an issue where the `event.getState()` of the preLoad event always comes down to null.
I was wondering if you’ve encountered something similar while working on a recent upgrade to 3.x?