Spring - Encrypt Properties by Customizing PropertySourcesPlaceholderConfigurer


Senario
Usually there are some sensitive properties(such as database password, aws key etc) in an application that we can't put it as plain text and push to git. We have to encrypt it, but decrypt when use it in the application.

Solution
We use some private password key to encrypt them, and put encrypted password in property file like below:
databse.password=ENC:encrypted_password

The ENC: prefix is used to tell the Spring application, this property is encrypted.

We pass the private password key to application server when start it by -DappPassword=password_key

Other approaches:
1. jasypt-spring-boot
You may consider to use jasypt-spring-boot in your sping-boot project. But I found one issue: By default it still decrypts the encrypted property every time when appContext.getEnvironment().getProperty is called for the same property.
com.ulisesbocchio.jasyptspringboot.resolver.DefaultPropertyResolver.resolvePropertyValue(String)

You may write your own MyEncryptablePropertyResolver to cache the already decrypted value in resolvePropertyValue.

2. PropertyPlaceholderConfigurer
Another option is to extend PropertyPlaceholderConfigurer
then implement methods convertPropertyValue, resolveSystemProperty, resolvePlaceholder to decrypt values.
The good part is it decrypt all values only once when spring creates PropertyPlaceholderConfigurer in PropertyResourceConfigurer.postProcessBeanFactory(ConfigurableListableBeanFactory). 

The bad part is that PropertyPlaceholderConfigurer is not EnvironmentAware which means we can not call appContext.getEnvironment().getProperty to get property value in static or non-spring-managed context.

Check the javadoc of PropertyPlaceholderConfigurer or PropertyResourceConfigurer:
As of Spring 3.1, PropertySourcesPlaceholderConfigurer should be used preferentially over this implementation; it is more flexible through taking advantage of the Environment and PropertySource mechanisms also made available in Spring 3.1.

How to Tell Spring to decrypt properties?
In Spring, we usually uses @PropertySource to specify property files. Then Spring uses PropertySourcesPlaceholderConfigurer to read them.

All we have to do is extend PropertySourcesPlaceholderConfigurer, so it(StringValueResolver) will decrypt property value when the values matches some pattern.

Problem of PropertySourcesPlaceholderConfigurer
One issue about PropertySourcesPlaceholderConfigurer: it handles @Value and appContext.getEnvironment().getProperty differently.

To decrypt value for placeholder in @Value, we can define our our StringValueResolver like below.

For @value, when spring tries to create the bean, it will call ValueResolver.resolveStringValue to parse it. We can define our our EncryptedValueResolver to decrypt value for placeholder.
EncryptedValueResolver.resolveStringValue(String) line: 32
DefaultListableBeanFactory(AbstractBeanFactory).resolveEmbeddedValue(String) line: 823
DefaultListableBeanFactory.doResolveDependency(DependencyDescriptor, String, Set, TypeConverter) line: 1084

DefaultListableBeanFactory.resolveDependency(DependencyDescriptor, String, Set, TypeConverter) line: 1064

But when you call appContext.getEnvironment().getProperty, the value is still not decrypted. One approach is to create one util SpringContextBridge, whose getProperty will decrypt the property value. 

Also we define our decrypt method to cache already decrypted value in a map.

The Implementation
First we register our custom EncryptedPropertySourcesPlaceholderConfigurer in configuration.
Notice it has to be static method, this bean has to be created first.
    @Bean
    public static PropertySourcesPlaceholderConfigurer properties() {
        final String password = System.getProperty(APP_ENCRYPTION_PASSWORD);
        if (StringUtils.isBlank(password)) {
            return new PropertySourcesPlaceholderConfigurer();
        } else {
            return new EncryptedPropertySourcesPlaceholderConfigurer(password);
        }
    }
Here we are using jasypt's BasicTextEncryptor, you are free to use any encryptor.
public class EncryptedPropertySourcesPlaceholderConfigurer extends PropertySourcesPlaceholderConfigurer {
    private final String password;

    public EncryptedPropertySourcesPlaceholderConfigurer(final String password) {
        super();
        this.password = password;
    }

    @Override
    protected void doProcessProperties(final ConfigurableListableBeanFactory beanFactoryToProcess,
            final StringValueResolver valueResolver) {
        super.doProcessProperties(beanFactoryToProcess, new EncryptedValueResolver(valueResolver, password));
    }
}
public class EncryptedValueResolver implements StringValueResolver {

    public static final String ENCRYPTED_PREFIX = "ENC:";

    private StringValueResolver valueResolver;

    private static PBEStringEncryptor encryptor;

    // Here we can use different encryptor
    // don't use StrongTextEncryptor, unless u have installed the Java Cryptography
    // Extension (JCE) Unlimited Strength Jurisdiction Policy Files in this jvm.
    EncryptedValueResolver(final StringValueResolver stringValueResolver, final String password) {
        this.valueResolver = stringValueResolver;
        encryptor = getEncryptor(password);
    }

    @Override
    public String resolveStringValue(final String strVal) {

        // Values obtained from the property file to the naming
        // as seen with the encryption target
        String value = valueResolver.resolveStringValue(strVal);
        value = decrypt(value);
        return value;
    }

    private static Map<String, String> decryptValues = new HashMap<>();

    public static String decrypt(String originalValue) {
        if (originalValue != null && originalValue.startsWith(ENCRYPTED_PREFIX)) {
            return decryptValues.computeIfAbsent(originalValue,
                    oldValue -> encryptor.decrypt(oldValue.substring(ENCRYPTED_PREFIX.length())));
        }
        return originalValue;
    }

    private static final String SALT = "YOUR_SALT_HERE";

    public static StandardPBEStringEncryptor getEncryptor(final String password) {
        final StandardPBEStringEncryptor encryptor = new StandardPBEStringEncryptor();
        encryptor.setPassword(password); // we HAVE TO set a password
        // use default algorithm
        // don't use PBEWithMD5AndTripleDES
        encryptor.setAlgorithm("PBEWithMD5AndDES");

        final StringFixedSaltGenerator saltGenerator = new StringFixedSaltGenerator(SALT);
        encryptor.setSaltGenerator(saltGenerator);
        return encryptor;
    }

Problem in previous
There is one problem in previous code 


EncryptorUtil
Last, EncryptorUtil will use our private password key to encrypt text.
public class EncryptorUtil {
  protected static void decrypt(final String password, final String encryptedMessage) {
      final StandardPBEStringEncryptor encryptor = EncryptedValueResolver.getEncryptor(password);

      // don't use BasicTextEncryptor, as it's salt changes.
      // final BasicTextEncryptor textEncryptor = new BasicTextEncryptor();
      // textEncryptor.setPassword(password);
      final String plainText = encryptor.decrypt(encryptedMessage);

      System.out.println("plainText: " + plainText);

  }

  protected static void encrypt(final String password, final String plainText) {
      final StandardPBEStringEncryptor encryptor = EncryptedValueResolver.getEncryptor(password);

      // don't use BasicTextEncryptor, as it's salt changes.
      // final BasicTextEncryptor textEncryptor = new BasicTextEncryptor();
      // textEncryptor.setPassword(password);
      final String myEncryptedText = encryptor.encrypt(plainText);

      System.out.println("Encrypted value: " + EncryptedValueResolver.ENCRYPTED_PREFIX + myEncryptedText);
      // make sure we can decrypt from the encrypted text,
      System.out.println(
              "Decrypted value matches the actual value: " + plainText.equals(encryptor.decrypt(myEncryptedText)));
  }
  public static void main(final String[] args) {
       if (args.length < 3) {
       System.out.println("Please input the password(even length) and the text to be encrypted.");
       return;
       }
       final String action = args[0], password = args[1], text = args[2];
       if ("enc".equalsIgnoreCase(action)) {
       encrypt(password, text);
       } else {
       decrypt(password, text);
       }
  }
}
Resources
5.10. Properties Management
Extending Spring PropertyPlaceholderConfigurer to consider the OS Platform

Labels

adsense (5) Algorithm (69) Algorithm Series (35) Android (7) ANT (6) bat (8) Big Data (7) Blogger (14) Bugs (6) Cache (5) Chrome (19) Code Example (29) Code Quality (7) Coding Skills (5) Database (7) Debug (16) Design (5) Dev Tips (63) Eclipse (32) Git (5) Google (33) Guava (7) How to (9) Http Client (8) IDE (7) Interview (88) J2EE (13) J2SE (49) Java (186) JavaScript (27) JSON (7) Learning code (9) Lesson Learned (6) Linux (26) Lucene-Solr (112) Mac (10) Maven (8) Network (9) Nutch2 (18) Performance (9) PowerShell (11) Problem Solving (11) Programmer Skills (6) regex (5) Scala (6) Security (9) Soft Skills (38) Spring (22) System Design (11) Testing (7) Text Mining (14) Tips (17) Tools (24) Troubleshooting (29) UIMA (9) Web Development (19) Windows (21) xml (5)