Replacement for `hydrate` in Hibernate 6.1

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?

You could also map the encryption key id multiple times as read-only in the embeddable if you want e.g.

@Column(insertable = false, updatable = false)
private String encryptionKeyId;

Alternatively, you can also inject the parent entity to avoid messing around with encrypt/decrypt manually.