Java hibernate conditionally apply annotation to a field?

Viewed 949

Scenario: A data object which persists in the DB table. There are some old entries in the table. Now I have to apply encryption to new further entries in the table. So I add a new column which has the field encrypted set to False by default to check if the values are encrypted.

Problem: I want to write an annotation to encrypt the fields in the data model(POJO) before persisting and decrypt on getter() calls only if it is encrypted.

Context:

The user model.

public class UserData {
    @Id
    @Column(name = "ID", length = 36)
    private String id;
    
    @Column(name = "IS_ENCRYPTED")
    private boolean isEncrypted;

    @Column(name = "NAME")
    @Convert(converter = EncryptionConverter.class)
    private String name;
   // more fields ....
    
    public String getId() {
       return id;
    }
    public void setId(String id) {
        this.id = id;
    }
    // more similar getter and setters
}

The encryption class that i have written.

@Converter
    public class EncryptionConverter implements AttributeConverter<String, String>{
        private final String  secretKey= "someSecret";
        UserData Data = new UserData();
        @Override
        public  String convertToDatabaseColumn(String str) {
            if(!isNullOrBlank(str))
                return AesEncrypt.encrypt(str, secretKey);
            return str;
        }
        @Override
        public String convertToEntityAttribute(String encrypedStr) {
            if(!isNullOrBlank(encrypedStr) && Data.isEncrypted)
                return AesEncrypt.decrypt(encrypedStr, secretKey);
            return encrypedStr;
        }
        
    }

This class is inside the model class. (can move outside, but how to pass isencrypted flag to annotation)

How can I do this, is my approach correct?

Edit: there are multiple fields which are to be encrypted/decrypted not just name.

3 Answers

You can create the encryption behaviour in another configuration class, say EncryptedPropertyConfig, in this you can create a bean, EncryptablePropertyResolver from jasypt-spring-boot

@EnableAutoConfiguration
public class EncryptedPropertyConfig {
    
    public EncryptedPropertyConfig() {
    }

    @Bean
    public EncryptablePropertyResolver encryptablePropertyResolver() {
        EncryptablePropertyResolver r = new MyPropertyPlaceholderConfigurer();
        return r;
    }
}

public final class MyPropertyPlaceholderConfigurer implements EncryptablePropertyResolver {
    private StandardPBEStringEncryptor encryptor = new StandardPBEStringEncryptor();
    private EnvironmentStringPBEConfig envConfig = new EnvironmentStringPBEConfig();

 
    public MyPropertyPlaceholderConfigurer() {
     // set the encryption key and config
    }
     
    public String resolvePropertyValue(String passedValue) {
        if (!PropertyValueEncryptionUtils.isEncryptedValue(passedValue)) {
            return passedValue;
        } else {
            String returnValue = "";

            try {
                returnValue = PropertyValueEncryptionUtils.decrypt(passedValue, this.encryptor);
                return returnValue;
            } catch (Exception var4) {
                throw new RuntimeException("Error in decryption of property value:" + passedValue, var4);
            }
        }
    }
}

I suggest alternative solution using Entity Listeners

import javax.persistence.PostLoad;
import javax.persistence.PreUpdate;

public class UserData {

    private final String  secretKey= "someSecret";

    // ... 
    
    @PreUpdate
    private void onUpdate() {
        // triggered before saving entity to DB (both create & update)
        if(!isNullOrBlank(name)) {
            name = AesEncrypt.encrypt(name, secretKey);
        }
    }

    @PostLoad
    private void onLoad() {
        // triggered after entity is fetched from Entity Provider
        if (!isNullOrBlank(name) && isEncrypted) {
            name = AesEncrypt.decrypt(name, secretKey);
        }
    }
}

Instead of using JPA AttributeConverter you can implement hibernate user type in this way:

import java.util.Objects;
import org.hibernate.HibernateException;
import org.hibernate.engine.spi.SharedSessionContractImplementor;
import org.hibernate.type.StringType;
import org.hibernate.usertype.UserType;

public class CustomNameType implements UserType
{
   private String secretKey = "someSecret";

   public CustomNameType()
   {
   }

   @Override
   public Object deepCopy(Object value) throws HibernateException
   {
      if (null == value) return null;
      return ((CustomName) value).clone();
   }

   @Override
   public Object assemble(Serializable cached, Object owner) throws HibernateException
   {
      return cached;
   }

   @Override
   public Serializable disassemble(Object value) throws HibernateException
   {
      return (Serializable) value;
   }

   @Override
   public Object replace(Object original, Object target, Object owner) throws HibernateException
   {
      return original;
   }

   @Override
   public boolean equals(Object one, Object two) throws HibernateException
   {
      return Objects.equals(one, two);
   }

   @Override
   public int hashCode(Object obj) throws HibernateException
   {
      return Objects.hashCode(obj);
   }

   @Override
   public boolean isMutable()
   {
      return true;
   }

   @Override
   public Object nullSafeGet(ResultSet rs, String[] names, SharedSessionContractImplementor session, Object owner)
      throws HibernateException, SQLException
   {
      boolean isEncrypted = rs.getBoolean(0); // IS_ENCRYPTED
      String name = rs.getString(1);          // NAME
      if (isEncrypted) {
         name = AesEncrypt.decrypt(name, secretKey);
      }
      
      return new CustomName(isEncrypted, name);
   }

   @Override
   public void nullSafeSet(PreparedStatement statement, Object value, int index, SharedSessionContractImplementor session)
         throws HibernateException, SQLException
   {
      CustomName customName = (CustomName) value;
      String name = customName.getName();
      if (customName.isEncrypted()) {
         name = AesEncrypt.encrypt(name, secretKey);
      }
      
      statement.setBoolean(0, customName.isEncrypted());
      statement.setString(1, name);
   }

   @Override
   public Class<?> returnedClass()
   {
      return CustomName.class;
   }

   @Override
   public int[] sqlTypes()
   {
      // I do not know the types of your IS_ENCRYPTED and NAME fields
      // So, this place maybe require correction
      int[] types = {BooleanType.INSTANCE.sqlType(), StringType.INSTANCE.sqlType()};
      return types;
   }
   
}

where CustomName is:

public class CustomName implements Serializable, Cloneable
{
   private boolean isEncrypted;
   private String name;

   public CustomName(boolean isEncrypted, String name)
   {
      this.isEncrypted = isEncrypted;
      this.name = name;
   }
   
   // getters , equals, hashCode ...
   
   @Override
   public CustomName clone()
   {
      return new CustomName(isEncrypted, name);
   }
}

and then use it:

import org.hibernate.annotations.Type;
import org.hibernate.annotations.Columns;

@Entity
public class UserData {

   @Type(type = "com.your.CustomNameType")
   @Columns(columns = {
      @Column(name = "IS_ENCRYPTED"),
      @Column(name = "NAME")
   })
   private CustomName name;
}
Related