Still having problems implementing the right/corrent solution.
The encryption key (or the id of the key) can’t be part of the @Embeddable since there are several encrypted properties per Entity.
I am trying to encrypt/decrypt onPreUpdate/onPreInsert/onPostLoad Listeners.
onPostLoad works well, but when setting the transient value of the embedded EncryptedString the transient value is sometimes not present. I think this is the case when the Entity was loaded with a null value which is then updated in the transaction.
@Embeddable
public class EncryptedString {
public static final String NAME = "encryptedValue";
private String encryptedValue = null;
@Transient
private String plainValue = null;
public EncryptedString() {
}
public EncryptedString(String plainValue) {
this.plainValue = plainValue;
}
public void setValue(String data) {
this.plainValue = data;
this.encryptedValue = null;
}
public String getValue() {
return this.plainValue;
}
void encrypt(UnaryOperator<String> encryptor) {
if (plainValue != null && encryptedValue == null) {
encryptedValue = encryptor.apply(plainValue);
} else if (plainValue == null && encryptedValue != null) {
encryptedValue = null;
}
}
void decrypt(UnaryOperator<String> decryptor) {
if (encryptedValue != null && plainValue == null) {
plainValue = decryptor.apply(encryptedValue);
} else if (encryptedValue == null && plainValue != null) {
plainValue = null;
}
}
}
@Entity
public class MyEntity implements EncryptorProvider {
@Embedded
@AttributeOverride(
name = EncryptedString.NAME,
column = @Column(name = "password")
)
private EncryptedString password;
public void setPassword(String password) {
if (this.password == null) {
this.password= new EncryptedString(password);
} else {
this.password.setValue(password);
}
}
}
public class EncryptionEventListener implements PreInsertEventListener, PreUpdateEventListener {
@Override
public boolean onPreUpdate(PreUpdateEvent event) {
processEncrypt(event, event.getState());
return false;
}
@Override
public boolean onPreInsert(PreInsertEvent event) {
processEncrypt(event, event.getState());
return false;
}
private void processEncrypt(AbstractPreDatabaseOperationEvent event, Object[] state) {
Object entity = event.getEntity();
if (entity instanceof EncryptorProvider provider) {
for (int i = 0; i < state.length; i++) {
if (state[i] instanceof EncryptedString encryptedString) {
// transient field encryptedString.plainValue is null here sometimes
encryptedString.encrypt(provider.getEncryptor()::encrypt);
}
}
}
}
}
So currently I ended up using a prefix in the encrypedValue to mark data which needs to be encrypted during the Listeners.
public class EncryptedString {
...
public void setValue(String data) {
this.plainValue = data;
this.encryptedValue = data == null ? null : UNENCRYPTED_PREFIX + data;
}
void encrypt(UnaryOperator<String> encryptor) {
if (plainValue != null && encryptedValue == null) {
encryptedValue = encryptor.apply(plainValue);
} else if (encryptedValue != null && !encryptedValue.isEmpty() && encryptedValue.charAt(0) == UNENCRYPTED_PREFIX) {
String restoredPlainValue = encryptedValue.substring(1);
if (plainValue == null) {
plainValue = restoredPlainValue;
}
encryptedValue = encryptor.apply(restoredPlainValue);
}
}
}
But this feels like a hack I am really open to get more pointers to do it the correct way…
- Use a Converter and push EncryptorProvider to a ThreadLocal within the Listeners!?
- Just drop the Embeddable und work with @Transient Fields directly?
- using an annotation @Encrypted(transientFieldName=“”) to mark and couple the persistent encrypted field and the unencrypted transient field
Any more hints or ideas?