Encrypting JPA entity attributes using listeners in Spring

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.

19 Replies to “Encrypting JPA entity attributes using listeners in Spring”

  1. Attila says:
    1. Arnold Galovics says:
  2. Joe Neuhaus says:
    1. Arnold Galovics says:
  3. Michał says:
      1. Arnold Galovics says:
    1. Arnold Galovics says:
  4. Tom says:
    1. Arnold Galovics says:
  5. Matthias says:
    1. Arnold Galovics says:
  6. Rostislav says:
    1. Arnold Galovics says:
  7. mike says:
  8. Aaron Verachtert says:
    1. Arnold Galovics says:
  9. bonjugi says:

Leave a Reply

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