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)); } } 1 2 3 4 5 6 @ 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); } } 1 2 3 4 5 6 @ 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 } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 @ 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 { } 1 2 3 4 @ 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; } } 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 @ 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()); } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 @ 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); } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 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())); } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 @ 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; } } 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 @ 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); } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 @ 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); }); } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 @ 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); }); } 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 @ 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.