Thursday 18 July 2013

Transparent DB encryption with Jasypt and JPA 2.1

Jasypt is tool written in Java to add transparent encryption to different parts of an application (e.g. database fields, properties files entries). The project website is located at: http://www.jasypt.org/ 

The following code shows how to support transparent encryption of some Database field using Jasypt in Eclipselink. Jasypt integrates well out of the box with Hibernate using some custom types supplied in the Jasypt distribution. There seems to be no equivalent support for Eclipselink.
My solution uses Eclipselink 2.5 and the new @Converter annotation introduced by the JPA 2.1 standard.

 package lb.util;  
   
 import javax.persistence.AttributeConverter;  
 import javax.persistence.Converter;  
   
   
 @Converter  
 public class EncryptedStringConverter implements AttributeConverter<String, String> {  
   
   private static final String STRING_ENCRYPTOR_NAME = "jpaStringEncryptor";  
   
   
   public EncryptedStringConverter() {  
   }  
   
   public String convertToDatabaseColumn(String value) {  
     if ( value == null ) {  
       return null;  
     }  
     return JpaPBEEncryptorRegistry.getInstance().getPBEStringEncryptor(STRING_ENCRYPTOR_NAME).encrypt(value);  
   }  
   
   public String convertToEntityAttribute(String value) {  
     if ( value == null ) {  
       return null;  
     }  
     return JpaPBEEncryptorRegistry.getInstance().getPBEStringEncryptor(STRING_ENCRYPTOR_NAME).decrypt(value);  
   }  
   
 }  
   


This is the only part that is strictly related to JPA. The rest of the code, which follows is ported from code available in the jasypt-hibernate-4 package.

 package lb.util;  
   
 import org.jasypt.encryption.pbe.PBEStringEncryptor;  
   
 import java.util.HashMap;  
   
   
 /**  
  * Maintains a registry of Jasypt JPA encryptors. Currently only String encryptor is supported.  
  * Derived from org.jasypt.hibernate4.encryptor.HibernatePBEEncryptorRegistry  
  *  
  */  
 public final class JpaPBEEncryptorRegistry {  
   
   
   // The singleton instance  
   private static final JpaPBEEncryptorRegistry instance =  
       new JpaPBEEncryptorRegistry();  
   
   
   // Registry maps  
   private final HashMap stringEncryptors = new HashMap();  
   
   
   /**  
    * Returns the singleton instance of the registry.  
    *  
    * @return the registry.  
    */  
   public static JpaPBEEncryptorRegistry getInstance() {  
     return instance;  
   }  
   
   // The registry cannot be externally instantiated.  
   private JpaPBEEncryptorRegistry() {  
     super();  
   }  
   
   
   /**  
    * Registers a <tt>PBEStringEncryptor</tt> object with the specified  
    * name.  
    *  
    * @param registeredName the registered name.  
    * @param encryptor the encryptor to be registered.  
    */  
   public synchronized void registerPBEStringEncryptor(  
       final String registeredName, final PBEStringEncryptor encryptor) {  
     final JpaPBEStringEncryptor jpaEncryptor =  
         new JpaPBEStringEncryptor(registeredName, encryptor);  
     this.stringEncryptors.put(registeredName, jpaEncryptor);  
   }  
   
   
   
   // Not public: this is used from  
   // JpaPBEStringEncryptor.setRegisteredName.  
   synchronized void registerJpaPBEStringEncryptor(  
       final JpaPBEStringEncryptor jpaEncryptor) {  
     this.stringEncryptors.put(  
         jpaEncryptor.getRegisteredName(),  
         jpaEncryptor);  
   }  
   
   
   // Not public: this is used from  
   // JpaPBEStringEncryptor.setRegisteredName.  
   synchronized void unregisterJpaPBEStringEncryptor(final String name) {  
     this.stringEncryptors.remove(name);  
   }  
   
   
   /**  
    * Returns the <tt>PBEStringEncryptor</tt> registered with the specified  
    * name (if exists).  
    *  
    * @param registeredName the name with which the desired encryptor was  
    *    registered.  
    * @return the encryptor, or null if no encryptor has been registered with  
    *     that name.  
    */  
   public synchronized PBEStringEncryptor getPBEStringEncryptor(  
       final String registeredName) {  
     final JpaPBEStringEncryptor jpaEncryptor =  
         (JpaPBEStringEncryptor) this.stringEncryptors.get(registeredName);  
     if (jpaEncryptor == null) {  
       return null;  
     }  
     return jpaEncryptor.getEncryptor();  
   }  
   
 }  
   


 package lb.util;  
   
 import org.jasypt.encryption.pbe.PBEStringEncryptor;  
 import org.jasypt.encryption.pbe.StandardPBEStringEncryptor;  
 import org.jasypt.encryption.pbe.config.PBEConfig;  
 import org.jasypt.exceptions.EncryptionInitializationException;  
 import org.jasypt.salt.SaltGenerator;  
   
 import java.security.Provider;  
   
   
 /**  
  * Jasypt String encryptor that can be used within a Jpa application.  
  * Derived from org.jasypt.hibernate4.encryptor.HibernatePBEStringEncryptor  
  *  
  */  
 public final class JpaPBEStringEncryptor {  
   
     private String registeredName = null;  
     private PBEStringEncryptor encryptor = null;  
     private boolean encryptorSet = false;  
   
   
     /**  
      * Creates a new instance of <tt>JpaPBEStringEncryptor</tt>. It also  
      * creates a <tt>StandardPBEStringEncryptor</tt> for internal use, which  
      * can be overriden by calling <tt>setEncryptor(...)</tt>.  
      */  
     public JpaPBEStringEncryptor() {  
       super();  
       this.encryptor = new StandardPBEStringEncryptor();  
       this.encryptorSet = false;  
     }  
   
   
     /*  
      * For internal use only, by the Registry, when a PBEStringEncryptor  
      * is registered programmatically.  
      */  
     JpaPBEStringEncryptor(final String registeredName,  
                   final PBEStringEncryptor encryptor) {  
       this.encryptor = encryptor;  
       this.registeredName = registeredName;  
       this.encryptorSet = true;  
     }  
   
   
     /**  
      * Returns the encryptor which this object wraps.  
      *  
      * @return the encryptor.  
      */  
     public synchronized PBEStringEncryptor getEncryptor() {  
       return this.encryptor;  
     }  
   
   
     /**  
      * Sets the <tt>PBEStringEncryptor</tt> to be held (wrapped) by this  
      * object. This method is optional and can be only called once.  
      *  
      * @param encryptor the encryptor.  
      */  
     public synchronized void setEncryptor(final PBEStringEncryptor encryptor) {  
       if (this.encryptorSet) {  
         throw new EncryptionInitializationException(  
             "An encryptor has been already set: no " +  
                 "further configuration possible on jpa wrapper");  
       }  
       this.encryptor = encryptor;  
       this.encryptorSet = true;  
     }  
   
   
     /**  
      * Sets the password to be used by the internal encryptor, if a specific  
      * encryptor has not been set with <tt>setEncryptor(...)</tt>.  
      *  
      * @param password the password to be set for the internal encryptor  
      */  
     public void setPassword(final String password) {  
       if (this.encryptorSet) {  
         throw new EncryptionInitializationException(  
             "An encryptor has been already set: no " +  
                 "further configuration possible on jpa wrapper");  
       }  
       final StandardPBEStringEncryptor standardPBEStringEncryptor =  
           (StandardPBEStringEncryptor) this.encryptor;  
       standardPBEStringEncryptor.setPassword(password);  
     }  
   
   
     /**  
      * Sets the password to be used by the internal encryptor (as a char[]), if a specific  
      * encryptor has not been set with <tt>setEncryptor(...)</tt>.  
      *  
      * @since 1.8  
      * @param password the password to be set for the internal encryptor  
      */  
     public void setPasswordCharArray(final char[] password) {  
       if (this.encryptorSet) {  
         throw new EncryptionInitializationException(  
             "An encryptor has been already set: no " +  
                 "further configuration possible on jpa wrapper");  
       }  
       final StandardPBEStringEncryptor standardPBEStringEncryptor =  
           (StandardPBEStringEncryptor) this.encryptor;  
       standardPBEStringEncryptor.setPasswordCharArray(password);  
     }  
   
   
     /**  
      * Sets the algorithm to be used by the internal encryptor, if a specific  
      * encryptor has not been set with <tt>setEncryptor(...)</tt>.  
      *  
      * @param algorithm the algorithm to be set for the internal encryptor  
      */  
     public void setAlgorithm(final String algorithm) {  
       if (this.encryptorSet) {  
         throw new EncryptionInitializationException(  
             "An encryptor has been already set: no " +  
                 "further configuration possible on jpa wrapper");  
       }  
       final StandardPBEStringEncryptor standardPBEStringEncryptor =  
           (StandardPBEStringEncryptor) this.encryptor;  
       standardPBEStringEncryptor.setAlgorithm(algorithm);  
     }  
   
   
     /**  
      * Sets the key obtention iterations to be used by the internal encryptor,  
      * if a specific encryptor has not been set with <tt>setEncryptor(...)</tt>.  
      *  
      * @param keyObtentionIterations to be set for the internal encryptor  
      */  
     public void setKeyObtentionIterations(final int keyObtentionIterations) {  
       if (this.encryptorSet) {  
         throw new EncryptionInitializationException(  
             "An encryptor has been already set: no " +  
                 "further configuration possible on jpa wrapper");  
       }  
       final StandardPBEStringEncryptor standardPBEStringEncryptor =  
           (StandardPBEStringEncryptor) this.encryptor;  
       standardPBEStringEncryptor.setKeyObtentionIterations(  
           keyObtentionIterations);  
     }  
   
   
     /**  
      * Sets the salt generator to be used by the internal encryptor,  
      * if a specific encryptor has not been set with <tt>setEncryptor(...)</tt>.  
      *  
      * @param saltGenerator the salt generator to be set for the internal  
      *           encryptor.  
      */  
     public void setSaltGenerator(final SaltGenerator saltGenerator) {  
       if (this.encryptorSet) {  
         throw new EncryptionInitializationException(  
             "An encryptor has been already set: no " +  
                 "further configuration possible on jpa wrapper");  
       }  
       final StandardPBEStringEncryptor standardPBEStringEncryptor =  
           (StandardPBEStringEncryptor) this.encryptor;  
       standardPBEStringEncryptor.setSaltGenerator(saltGenerator);  
     }  
   
   
     /**  
      * Sets the name of the JCE provider to be used by the internal encryptor,  
      * if a specific encryptor has not been set with <tt>setEncryptor(...)</tt>.  
      *  
      * @since 1.3  
      *  
      * @param providerName the name of the JCE provider (already registered)  
      */  
     public void setProviderName(final String providerName) {  
       if (this.encryptorSet) {  
         throw new EncryptionInitializationException(  
             "An encryptor has been already set: no " +  
                 "further configuration possible on jpa wrapper");  
       }  
       final StandardPBEStringEncryptor standardPBEStringEncryptor =  
           (StandardPBEStringEncryptor) this.encryptor;  
       standardPBEStringEncryptor.setProviderName(providerName);  
     }  
   
   
     /**  
      * Sets the JCE provider to be used by the internal encryptor,  
      * if a specific encryptor has not been set with <tt>setEncryptor(...)</tt>.  
      *  
      * @since 1.3  
      *  
      * @param provider the JCE provider to be used  
      */  
     public void setProvider(final Provider provider) {  
       if (this.encryptorSet) {  
         throw new EncryptionInitializationException(  
             "An encryptor has been already set: no " +  
                 "further configuration possible on jpa wrapper");  
       }  
       final StandardPBEStringEncryptor standardPBEStringEncryptor =  
           (StandardPBEStringEncryptor) this.encryptor;  
       standardPBEStringEncryptor.setProvider(provider);  
     }  
   
   
     /**  
      * Sets the type of String output ("base64" (default), "hexadecimal") to  
      * be used by the internal encryptor,  
      * if a specific encryptor has not been set with <tt>setEncryptor(...)</tt>.  
      *  
      * @since 1.3  
      *  
      * @param stringOutputType the type of String output  
      */  
     public void setStringOutputType(final String stringOutputType) {  
       if (this.encryptorSet) {  
         throw new EncryptionInitializationException(  
             "An encryptor has been already set: no " +  
                 "further configuration possible on jpa wrapper");  
       }  
       final StandardPBEStringEncryptor standardPBEStringEncryptor =  
           (StandardPBEStringEncryptor) this.encryptor;  
       standardPBEStringEncryptor.setStringOutputType(stringOutputType);  
     }  
   
   
     /**  
      * Sets the PBEConfig to be used by the internal encryptor,  
      * if a specific encryptor has not been set with <tt>setEncryptor(...)</tt>.  
      *  
      * @param config the PBEConfig to be set for the internal encryptor  
      */  
     public void setConfig(final PBEConfig config) {  
       if (this.encryptorSet) {  
         throw new EncryptionInitializationException(  
             "An encryptor has been already set: no " +  
                 "further configuration possible on jpa wrapper");  
       }  
       final StandardPBEStringEncryptor standardPBEStringEncryptor =  
           (StandardPBEStringEncryptor) this.encryptor;  
       standardPBEStringEncryptor.setConfig(config);  
     }  
   
   
     /**  
      * Encrypts a message, delegating to wrapped encryptor.  
      *  
      * @param message the message to be encrypted.  
      * @return the encryption result.  
      */  
     public String encrypt(final String message) {  
       if (this.encryptor == null) {  
         throw new EncryptionInitializationException(  
             "Encryptor has not been set into jpa wrapper");  
       }  
       return this.encryptor.encrypt(message);  
     }  
   
   
     /**  
      * Decypts a message, delegating to wrapped encryptor  
      *  
      * @param encryptedMessage the message to be decrypted.  
      * @return the result of decryption.  
      */  
     public String decrypt(final String encryptedMessage) {  
       if (this.encryptor == null) {  
         throw new EncryptionInitializationException(  
             "Encryptor has not been set into jpa wrapper");  
       }  
       return this.encryptor.decrypt(encryptedMessage);  
     }  
   
   
   
     /**  
      * Sets the registered name of the encryptor and adds it to the registry.  
      *  
      * @param registeredName the name with which the encryptor will be  
      *            registered.  
      */  
     public void setRegisteredName(final String registeredName) {  
       if (this.registeredName != null) {  
         // It had another name before, we have to clean  
         JpaPBEEncryptorRegistry.getInstance().  
             unregisterJpaPBEStringEncryptor(this.registeredName);  
       }  
       this.registeredName = registeredName;  
       JpaPBEEncryptorRegistry.getInstance().  
           registerJpaPBEStringEncryptor(this);  
     }  
   
     /**  
      * Returns the name with which the wrapped encryptor is registered at  
      * the registry.  
      *  
      * @return the registered name.  
      */  
     public String getRegisteredName() {  
       return this.registeredName;  
     }  
   
 }  
   


I've only ported the Jasypt String encryptor as that is all I needed, but other convertors can be easily ported from the corresponding Hibernate ones. Please refer to the original Jasypt source code for more information on how to port the other (e.g. byte[]) converters and add them to the registry. To configure transparent AES-256 encryption using spring xml then you would use the following:

   <!-- Jasypt Encryption/Decryption -->  
   <bean id="cryptProvider"  
      class="lb.security.BouncyCastleProvider" init-method="init"/>  
   
   
   <bean id="strongEncryptor"  
      class="org.jasypt.encryption.pbe.PooledPBEStringEncryptor">  
    <property name="algorithm">  
     <value>PBEWITHSHA256AND256BITAES-CBC-BC</value>  
    </property>  
    <property name="password">  
     <value>somepassword</value>  
    </property>  
    <property name="providerName">  
     <value>BC</value>  
    </property>  
    <property name="poolSize">  
     <value>4</value>  
    </property>  
   </bean>  
   
   <bean id="jpaStringEncryptor" class="lb.util.JpaPBEStringEncryptor" depends-on="cryptProvider,strongEncryptor">  
    <property name="registeredName">  
     <value>jpaStringEncryptor</value>  
    </property>  
    <property name="encryptor" ref="strongEncryptor"/>  
   </bean>  
   


For this to work you need Bouncycastle (I am using bcprov-jdk16.jar) in your classpath and also the Java Cryptography Extension (JCE) Unlimited Strength Jurisdiction Policy Files in your JRE lib/security directory. If you don't want to (or cannot) use Bouncycastle or the JCE extended policy files you will have to make the following changes in the Spring configuration: 
  • remove the "providerName" property;
  • remove the cryptProvider bean;
  • change the encryption algorithm to "PBEWithMD5AndDES"
Now String fields in JPA entities can be transparently encrypted by just annotating them as follows:

 @Convert(converter=EncryptedStringConverter.class)  
 @Column(length = 250)  
 private String someSecretField;  
   


No comments:

Post a Comment