Search code examples
javaspringspring-boothibernatespring-data-jpa

Save manipulated data with hibernate


I am trying to save user details into User table but with some columns having encrypted data, But I still want to use them after decrypting into the code. Example: User Details has username, I am encrypting and storing them in the table. But I want to use username after decrypting it.

To achieve this I added code to encrypt the username in setter of the bean and to use the value a code to decrypt the value is added in the getter, as below

@Table
@Entity
class User {

private String username;
private int age;

@Column
public String getUsername(){
 return Commons.decrypt(this.username);
}

@Column
public int getAge(){
 return this.age;
}

public void setUsername(String username){
 this.username = Commons.encrypt(username);
}

public void setAge(int age){}
 this.age = age;
}

And while saving the value in the User table, I am using persist method of javax.persistence.EntityManager class, code looks something like below

UserDetails user = new UserDetails();
user.setUsername("User 1");
user.setAge(31);
entityManager.persist();

I am expecting the code to perform as below

  1. When username is set through setter, it should encrypt the value
  2. When persist method is called encrypted value must be save in the DB
  3. When data is fetched from database it should have encrypted username value
  4. Upon calling getter method it should decrypt and return decrypted username

But the problem is as soon as I call the persist method hibernate internally calls getter and setter methods n number of times which finally results in saving a decrypted value i.e User 1 instead of eytsnkcjsdvnfsnvkscd= in the database, Hence when I try to get data from DB a decrypted value is returned and Commons.decrypt(this.username) in getter fails.

How to achieve the above expectations through a common code strategy, I was trying in getter and setter because most of the code is already is written and is using it. Making the change in single place i.e getter and setter will automatically enable security


Solution

  • With Spring Data, you would typically use a Repository Interface and do the encryption using an AttributeConverter.

    Here's an AttributeConverter example by sultanov.dev:

    @Component
    public class AttributeEncryptor implements AttributeConverter<String, String> {
    
        private static final String AES = "AES";
        private static final String SECRET = "secret-key-12345";
    
        private final Key key;
        private final Cipher cipher;
    
        public AttributeEncryptor() throws Exception {
            key = new SecretKeySpec(SECRET.getBytes(), AES);
            cipher = Cipher.getInstance(AES);
        }
    
        @Override
        public String convertToDatabaseColumn(String attribute) {
            try {
                cipher.init(Cipher.ENCRYPT_MODE, key);
                return Base64.getEncoder().encodeToString(cipher.doFinal(attribute.getBytes()));
            } catch (IllegalBlockSizeException | BadPaddingException | InvalidKeyException e) {
                throw new IllegalStateException(e);
            }
        }
    
        @Override
        public String convertToEntityAttribute(String dbData) {
            try {
                cipher.init(Cipher.DECRYPT_MODE, key);
                return new String(cipher.doFinal(Base64.getDecoder().decode(dbData)));
            } catch (InvalidKeyException | BadPaddingException | IllegalBlockSizeException e) {
                throw new IllegalStateException(e);
            }
        }
    }
    
    

    The entity User would then only contain the fundamental data fields and getter/setter methods (Lombok for brevity) and a @Convert annotation for the username.

    @Entity
    @Getter
    @Setter
    public class User {
        @Id
        private Long id;
    
        @Convert(converter = AttributeEncryptor.class)
        private String username;
    
        private int age;
    }
    

    The @Column annotation is used when an attribute in the entity is drawn from the secondary table, with a table attribute identifying the appropriate table. I'm not sure what you were trying to achieve marking the getter methods as columns.

    Access to stored objects is commonly achieved using one of Sping Data's Repository Interfaces, e.g. JpaRepository.

    public interface UserRepository extends JpaRepository<User, Long> {
        
    }
    

    In your Service Layer, you should then be able to work with data from repository with encryption being handled automatically.

    @Service
    public class UserService {
        @Autowired
        private UserRepository repository;
    
        @Transactional(readOnly = true)
        public User loadUser(Long id) {
            return repository.findById(id).orElse(null);;   
        }
    
        @Transactional  
        public User save(User user) {
            return repository.save(user);
        }
    }