JPA Injection in AttributeConverters

Java

Here is the use case. We have a table in the database that has a column where we keep a sensitive piece of information.
That is why this column was made of type VARBINARY and the data kept in it is encrypted. (using the AES algorithm)

 CREATE TABLE `example` (
  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
  `username` varchar(25) NOT NULL,
  `password` varbinary(200) NOT NULL
  PRIMARY KEY (`id`),
) ENGINE=InnoDB;

We wanted to be able to load the decrypted password directly into the JPA @Entity that is modeled according to that table.

@Entity
@Table(name = "example")
public class Example {

    @Id
    @GeneratedValue(strategy = IDENTITY)
    @Column(name = "id")
    private Integer id;

    @Column(name = "username", nullable = false, length = 25)
    private String username;

    @Column(name = "password", nullable = false)
    @Convert(converter = AesConverter.class)
    private char[] password;

    public Integer getId() {
        return this.id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public String getUsername() {
        return this.username;
    }

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

    public char[] getPassword() {
        return password != null ? password.clone() : null;
    }

    public void setPassword(char[] password) {
        this.password = password != null ? password.clone() : null;
    }
}    

As you can see we have a AesConverter class that is taking care of this. This is just an implementation of AttributeConverter that does the encryption/decryption, by delegating to another utility class AesUtil.

@Converter
public class AesConverter implements AttributeConverter<char[], byte[]> {

    @Resource(name = "dbEncryptionKey")
    private String dbEncryptionKey;

    @Override
    public byte[] convertToDatabaseColumn(char[] chars) {
        return AesUtil.encryptMySqlAes(chars, this.dbEncryptionKey);
    }

    @Override
    public char[] convertToEntityAttribute(byte[] bytes) {
        return AesUtil.decryptMySqlAes(bytes, this.dbEncryptionKey);
    }

}

In our example we use CDI and we configured the app in such a way that that the AesConverter.java file is picked up by the CDI container.
We are using Hibernate 5.3.x which implements JPA 2.2, so the bean named “dbEncryptionKey” which exists in the CDI context from a properties file just gets injected in our converter, so we are ready to go everything works like a charm.

Now, what to do, when using Hibernate 5.2.x or lower, or another JPA implementation that supports JPA 2.1 (did not upgrade to JPA 2.2)
Here is our solution.

@Converter
public class AesConverter implements AttributeConverter<char[], byte[]> {

    @Override
    public byte[] convertToDatabaseColumn(char[] chars) {
        return AES.getInstance().encrypt(chars);
    }

    @Override
    public char[] convertToEntityAttribute(byte[] bytes) {
        return AES.getInstance().decrypt(bytes);
    }

}

where AES.java is a singleton that uses a service locator to lazily load the needed encryption key from the CDI context only when first needed.

public class AES {

    private static final String DEFAULT_ENCRYPTION_KEY = "s3Cr!t";
    private String dbEncryptionKey = DEFAULT_ENCRYPTION_KEY;

    private static AES instance;

    private AES() {
        if (DEFAULT_ENCRYPTION_KEY.equals(this.dbEncryptionKey)) {
            this.getConfiguredEncryptionKey();
        }
    }    
    
    private AES(String dbEncryptionKey) {
        this.dbEncryptionKey = dbEncryptionKey;
    }

    public static AES getInstance(String dbEncryptionKey) {
        synchronized (AES.class) {
            if (instance == null) {
                instance = new AES(dbEncryptionKey);
            }
        }
        return instance;
    }

    public static AES getInstance() {
        synchronized (AES.class) {
            if (instance == null) {
                instance = new AES();
            }
        }
        return instance;
    }

    public char[] decrypt(byte[] bytes) {
        return AesUtil.decryptMySqlAesPassword(bytes, this.dbEncryptionKey);
    }

    public byte[] encrypt(char[] input) {
        return AesUtil.encryptMySqlAesPassword(input, this.dbEncryptionKey);
    }

    private void getConfiguredEncryptionKey() {
        try {
            this.mhqDbEncryptionKey = ContextLocator.getRegisteredBean("dbEncryptionKey");
        } catch (IllegalStateException isx) {
            isx.printStackTrace();
        }
    }
}