I am trying to encrypt several fields of domain objects before inserting / updating and decrypt them when choosing to display in the user interface.
I use Spring JPA data stores with Hibernate and EntityListener, which decrypt during the @PostLoad life cycle and encrypt during @PrePersist and @PreUpdate. The problem is that after the record is loaded from the database into the PersistenceContext, the listener decrypts the data, which makes EntityManager think that the object has been changed, which in turn causes updating again and, therefore, @PreUpdate encryption. Any tips 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 individual objects from a 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 JPA Data Warehouse
public interface MyCaseRepository extends JpaRepository<MyCase, String> {
}
Interface MyCaseService
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);
}
}
JPA Encryption 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>>();
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();
}
}
}
}