I'm trying to encrypt a handful of fields on domain entities before insert/update and decrypt them upon select to display in the UI.
I'm using Spring Data JPA repositories with Hibernate and an EntityListener which decrypts during @PostLoad lifecycle event and encrypts during @PrePersist and @PreUpdate. The problem I have is that once the record is loaded from the DB into the PersistenceContext, the listener decrypts the data which makes the EntityManager think the entity has been altered which in turn triggers an Update and hence @PreUpdate encryption again. Any advice on how to handle this?
- Spring 4.0.4.RELEASE
- Spring Data JPA 1.5.2.RELEASE
- Hibernate 4.2.14.Final
Is there an easy way to return detached entities from the JPA Repository?
Entity Class
@Entity
@Table(name="cases")
@EntityListeners(EncryptionListener.class)
public class MyCase implements Serializable, EncryptionEntity {
private static final Logger logger = LoggerFactory.getLogger(MyCase.class);
private static final long serialVersionUID = 1L;
private String caseNumber;
private byte[] secretProperty;
private byte[] iv;
@Id
@Column(name="case_number")
public String getCaseNumber() {
return caseNumber;
}
public void setCaseNumber(String caseNumber) {
this.caseNumber = caseNumber;
}
@Column(name="secret_property")
public byte[] getSecretProperty() {
return secretProperty;
}
public void setSecretProperty(byte[] secretProperty) {
this.secretProperty = secretProperty;
}
@Column
public byte[] getIv() {
return iv;
}
public void setIv(byte[] iv) {
this.iv = iv;
}
@Override
@Transient
public byte[] getInitializationVector() {
return this.iv;
}
@Override
public void setInitializationVector(byte[] iv) {
this.setIv(iv);
}
}
EncryptionEntity Interface
public interface EncryptionEntity {
public byte[] getInitializationVector();
public void setInitializationVector(byte[] iv);
}
Spring Data JPA Repository
public interface MyCaseRepository extends JpaRepository<MyCase, String> {
}
MyCaseService Interface
public interface MyCaseService {
public MyCase findOne(String caseNumber);
public MyCase save(MyCase case);
}
MyCaseService Implementation
public class MyCaseServiceImpl implements MyCaseService {
private static final Logger logger = LoggerFactory.getLogger(MyCaseServiceImpl.class);
@Autowired
private MyCaseRepository repos;
@Override
public MyCase findOne(String caseNumber) {
return repos.findOne(caseNumber);
}
@Transactional(readOnly=false)
public MyCase save(MyCase case) {
return repos.save(case);
}
}
Encryption JPA Listener Class
@Component
public class EncryptionListener {
private static final Logger logger = LoggerFactory.getLogger(EncryptionListener.class);
private static EncryptionUtils encryptionUtils;
private static SecureRandom secureRandom;
private static Map<Class<? extends EncryptionEntity>,
List<EncryptionEntityProperty>> propertiesToEncrypt;
@Autowired
public void setCrypto(EncryptionUtils encryptionUtils){
EncryptionListener.encryptionUtils = encryptionUtils;
}
@Autowired
public void setSecureRandom(SecureRandom secureRandom){
EncryptionListener.secureRandom = secureRandom;
}
public EncryptionListener(){
if (propertiesToEncrypt == null){
propertiesToEncrypt = new HashMap<Class<? extends EncryptionEntity>, List<EncryptionEntityProperty>>();
//MY CASE
List<EncryptionEntityProperty> propertyList = new ArrayList<EncryptionEntityProperty>();
propertyList.add(new EncryptionEntityProperty(MyCase.class, "secretProperty", byte[].class));
propertiesToEncrypt.put(MyCase.class, propertyList);
}
}
@PrePersist
public void prePersistEncryption(EncryptionEntity entity){
logger.debug("PRE-PERSIST");
encryptFields(entity);
}
@PreUpdate
public void preUpdateEncryption(EncryptionEntity entity){
logger.debug("PRE-UPDATE");
encryptFields(entity);
}
public void encryptFields(EncryptionEntity entity){
byte[] iv = new byte[16];
secureRandom.nextBytes(iv);
encryptionUtils.setIv(iv);
entity.setInitializationVector(iv);
logger.debug("Encrypting " + entity);
Class<? extends EncryptionEntity> entityClass = entity.getClass();
List<EncryptionEntityProperty> properties = propertiesToEncrypt.get(entityClass);
for (EncryptionEntityProperty property : properties){
logger.debug("Encrypting '{}' field of {}", property.getName(), entityClass.getSimpleName());
if (property.isEncryptedWithIv() == false){
logger.debug("Encrypting '{}' without IV.", property.getName());
}
try {
byte[] bytesToEncrypt = (byte[]) property.getGetter().invoke(entity, (Object[]) null);
if (bytesToEncrypt == null || bytesToEncrypt.length == 0){
continue;
}
byte[] encrypted = encryptionUtils.encrypt(bytesToEncrypt, property.isEncryptedWithIv());
property.getSetter().invoke(entity, new Object[]{encrypted});
} catch (Exception e){
logger.error("Error while encrypting '{}' property of {}: " + e.getMessage(), property.getName(), entityClass.toString());
e.printStackTrace();
}
}
}
@PostLoad
public void decryptFields(EncryptionEntity entity){
logger.debug("POST-LOAD");
logger.debug("Decrypting " + entity);
Class<? extends EncryptionEntity> entityClass = entity.getClass();
byte[] iv = entity.getInitializationVector();
List<EncryptionEntityProperty> properties = propertiesToEncrypt.get(entityClass);
for (EncryptionEntityProperty property : properties){
try {
byte[] value = (byte[]) property.getGetter().invoke(entity, (Object[]) null);
if (value == null || value.length == 0){
logger.debug("Ignoring blank field {} of {}", property.getName(), entityClass.getSimpleName());
continue;
}
logger.debug("Decrypting '{}' field of {}", property.getName(), entityClass.getSimpleName());
if (property.isEncryptedWithIv() == false){
logger.debug("Decrypting '{}' without IV.", property.getName());
}
byte[] decrypted = encryptionUtils.decrypt(value, iv, property.isEncryptedWithIv());
property.getSetter().invoke(entity, new Object[]{decrypted});
} catch (Exception e){
logger.error("Error while decrypting '{}' property of {}", property.getName(), entityClass.toString());
e.printStackTrace();
}
}
}
}
@PostLoad
invoked while callingfindOne(String caseNumber)
method ? – wypieprzMyCaseServiceImpl.findOne
method outside of the persistence-context? The entities should be returned then as detached ones. – wypieprz@Transactional
? I've moved on to a different approach using my EncryptionListener as an injected component to encrypt/decrypt. But it is certainly not as elegant as the transparent encryption/decryption I was hoping for. – Jay Goettelmann@Transactional(propagation = Propagation.NOT_SUPPORTED)
will force detachment. – wypieprz