Using Hibernate EventListeners to encrypt / decrypt properties of an entity based on other properties

Update 2012/07/24:
It turns out my approach below is NOT GOOD! What I wanted to do is conditional encryption which is also asked in this Jasypt forum, where they say one should implement your own UserType.
http://forum.jasypt.org/Hibernate-and-Conditional-encryption-td5586032.html

The serious problem of my approach is decribed at the end. It is that the onLoad Listener calls a setter of the entity for decryption and thus marks it as dirty which causes Hibernate to persist the changed state to DB again, which is not what you want. Seems the EventListener approach is too late in the chain and UserTypes is the way to go. Unfortunatelly it is lots of code which I would need to borrow from Jasypt in this case just for a tiny extension. Let’s see if I can maybe contribute it back to Jasypt.

Here is the original entry:
We are currenlty introducing the Jasypt library in a project @Synesty in order to encrypt sensitive values in .properties files and more important for sensitive user-content in the database. Jasypt brings stuff to make this easier. But (I think) we need to do something which goes beyond the stuff what is super-simple in Jasypt.

What we have:

We have an entity JobProperty(String key, String value, boolean isAlreadyEncrypted)

What we want to do when the entity is saved/updated:

prop.setValue(encryper.encrypt(prop.getValue());
prop.setIsAlreadyEncrypted(true);

What we want to do when the entity is loaded:

if(prop.isAlreadyEncrypted == true) {
prop.setValue(encryper.decrypt(prop.getValue());
}

Basically what we want to do is encrypting a property based on one (or more) other properties, in this case depending on the property ‚isAlreadyEncoded‘, which is false for legacy data before encryption was introduced. Those legacy entities are not encrypted yet, thus the code needs to know that those values cannot be decrypted.
Maybe in the future we are adding more stuff e.g. we want the user to be able to decide whether or not to encrypt, so there could be another property which we are checking in order to decide whether or not to encrypt. For those readers who might wonder why we are not using the Hibernate Custom UserType approach by Jasypt: The reason is, that this approach does not support our scenario where encryption should depend on other properties of the entity. It works great though if you always want to encrypt some property. In that case the described Hibernate UserType approach works great. But we need more flexibility and also backwards compatibility for existing non-encrypted data and also for our planned support for seemless key rotation via key-profiles.

Our approach: Hibernate Event Listeners (SaveOrUpdateEventListener, PostLoadEventListener)

We are basically using the Hibernate Event System so that we can hook into the different phases of Hibernates Entity lifecycle (a full list of Events can be found here or examples here). This approach can also be used for another common pattern, which is to maintain an „created or last-updated timestamp“ field on all entities every time the entity gets updated.

We are basically hooking into the „save-update“ (to encrypt before persisting the entity) and „post-load“ (to decrypt after loading the entity) event.

The problem

One problem we had was the onPostLoad event, which was persisting the data again to the database after decrypting the value. This is not what we wanted.
What we wanted was that the decrypted value is just like a transient value for displaying it in the UI. The underlying database value should still be encrypted.

The solution

Our current solution for this problem is the org.hibernate.Session.setReadOnly(entity, true) method.

/**
* Set an unmodified persistent object to read only mode, or a read only
* object to modifiable mode. In read only mode, no snapshot is maintained
* and the instance is never dirty checked.
*
* @see Query#setReadOnly(boolean)
*/
public void setReadOnly(Object entity, boolean readOnly);

So basically what we are doing in pseudo-code is:

if(prop.isAlreadyEncrypted == true) {
event.getSession().setReadOnly(prop, true);  // mark the object as readonly for the current session, because otherwise hibernate will persist the decrypted value to db again.
prop.setValue(encryper.decrypt(prop.getValue());
}

Show me a full example

In our case the listener looks like this:

 

public class MyLoadInsertUpdateListener extends DefaultSaveOrUpdateEventListener implements PostLoadEventListener, SaveOrUpdateEventListener{
 
	@Override
	public void onSaveOrUpdate(SaveOrUpdateEvent arg0) {
		if(arg0.getObject() instanceof Jobsproperties){
			Jobsproperties p = (Jobsproperties) arg0.getObject();
 
				EncryptionService encService = Activator.getServiceFactory().getService(EncryptionService.class);
				if(p.getJpvalue() != null){
					p.setJpvalue(encService.encrypt(p.getJpvalue()));
					p.setIsEncrypted(1);
				}
 
		}
 
		if(arg0.getObject() instanceof JobStepProperties){
			JobStepProperties p = (JobStepProperties) arg0.getObject();
 
				EncryptionService encService = Activator.getServiceFactory().getService(EncryptionService.class);
				if(p.getJspvalue() != null){
					p.setJspvalue(encService.encrypt(p.getJspvalue()));
					p.setIsEncrypted(1);
				}
 
		}
 
		super.onSaveOrUpdate(arg0);
	}
 
	@Override
	public void onPostLoad(PostLoadEvent arg0) {
 
		if(arg0.getEntity() instanceof Jobsproperties){
			Jobsproperties p = (Jobsproperties) arg0.getEntity();
			if(p.getIsEncrypted() == 1){
				if(p.getJpvalue() != null){
					EncryptionService encService = Activator.getServiceFactory().getService(EncryptionService.class);
 
					// workaround: mark the object as readonly, because otherwise hibernate will persist the decrypted value to db again.
					arg0.getSession().setReadOnly(p, true);
 
					p.setJpvalue(encService.decrypt(p.getJpvalue()));
 
				}
			}
		}
 
		if(arg0.getEntity() instanceof JobStepProperties){
			JobStepProperties p = (JobStepProperties) arg0.getEntity();
			if(p.getIsEncrypted() == 1){
				if(p.getJspvalue() != null){
					EncryptionService encService = Activator.getServiceFactory().getService(EncryptionService.class);
 
					// workaround: mark the object as readonly, because otherwise hibernate will persist the decrypted value to db again.
					arg0.getSession().setReadOnly(p, true);
					p.setJspvalue(encService.decrypt(p.getJspvalue()));
 
				}
			}
		}
 
	}
 
}

The used EncryptionService is just an internal interface implementation which provides an encrypt/decrypt method, but it is not important in this post.

As we are using Spring in combination with Hibernate it might be also interesting to see how our listener is registered, because there are also some pitfalls:

			.... our classes
 
				... hibernate properties
<map>
</map>

If you are wondering why we registed the same class 4 times as 4 beans? Hibernates doc states:

Listeners registered declaratively cannot share instances. If the same class name is used in multiple elements, each reference will result in a separate instance of that class.

With this approach so far we managed to solve our requirement but we are still testing it. One proplem we are seeing with this approach is that the Session.setReadOnly() method does have some code smell and is maybe a hack. I think in cases where we are just loading an entity for display purposes it works fine. But in cases where we are loading the entity, performing some update and then persisting the entity we could run into problems because of the read-only mode. But so far this concern has not settled yet although we clearly have load-update-persist patterns in the code.

Alternatives

What alternatives did we have and why did we go this way?

I think the alternative would have been to maybe also implement a custom Hibernate UserType. Our first idea was to extend the Jasypt’s org.jasypt.hibernate3.type.AbstractEncryptedAsStringType but unfortunatelly all the interesting methods are declared final so the only option would be to copy & paste the whole class…. but this should be our last resort if there is no other way.

If anybody has suggestions for a better solution please comment or get in touch with me.

I hope this post is somehow helpful for you to either go down the same road or get inspiration for different approaches and the related problems around it.

Dieser Beitrag wurde unter Software-Development abgelegt und mit , , , , verschlagwortet. Setze ein Lesezeichen auf den Permalink.