Safe implementation of automatic encryption and decryption of SpringBoot configuration files

Requirement Background

During application development, there are often some sensitive configuration properties

  • Database account and password
  • Third-party service account password
  • Built-in encrypted password
  • Other sensitive configuration

For companies with relatively high security requirements, sensitive configurations are often not allowed to appear in plain text.
It is common practice to encrypt these sensitive configurations and then decrypt them where they are used. However, some third-party configurations may not provide decryption injection points such as database passwords, so it is more troublesome to implement at this time. Is there a more convenient way to automatically identify and decrypt.
This time, we mainly aim at this problem and solve the encryption problem of sensitive configuration.

Implementation ideas

  • Use existing third-party packages: such as jasypt-spring-boot
    • This is a package for encryption and decryption for SpringBoot project configuration, which can be implemented by introducing dependencies in the project. Search by yourself for specific usage
  • Refer to the official documentation and use the official extension points to implement it yourself
    • Implement EnvironmentPostProcessor
      • EnvironmentPostProcessor is called after the configuration file is parsed and before the bean is created
    • Implement BeanFactoryPostProcessor (preferred)
      • BeanFactoryPostProcessor is called after the configuration file is parsed and before the bean is created
      • The implementation method is basically the same as EnvironmentPostProcessor, and the injection timing is later.

Solved by implementing EnvironmentPostProcessor

Implement EnvironmentPostProcessor to customize the environment configuration processing logic. The implementation example is as follows

 @Override
    public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) {
        MutablePropertySources mutablePropertySources = environment. getPropertySources();

        for (PropertySource<?> propertySource : mutablePropertySources) {
            if (propertySource instanceof OriginTrackedMapPropertySource) {
                mutablePropertySources.replace(propertySource.getName(),
                // Implement a wrapper class for dynamic judgment
                        new PropertySourceWrapper(propertySource, initSimpleEncryptor("reduck-project")
                                , new EncryptionWrapperDetector("$ENC{", "}"))
                );
            }
        }
    }

EnvironmentPostProcessor can also automatically expand the configuration file. If some projects implement their own configuration loading logic at this extension point, the order may need to be considered. Here it is recommended to implement BeanFactoryPostProcessor, which is called after the relevant instance of EnvironmentPostProcessor is processed, and before the Bean is created. Can better meet the demand.

Solution by implementing BeanFactoryPostProcessor

  • Implement EncryptionBeanPostProcessor
    • Generally, OriginTrackedMapPropertySource is our custom configuration loading instance, which replaces the original instance through a wrapper class
@RequiredArgsConstructor
public class EncryptionBeanPostProcessor implements BeanFactoryPostProcessor, Ordered {
    private final ConfigurableEnvironment environment;

    @Override
    public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
        MutablePropertySources mutablePropertySources = environment. getPropertySources();

        String secretKey = environment.getProperty("configuration.crypto.secret-key");

        if(secretKey == null) {
            return;
        }

        for (PropertySource<?> propertySource : mutablePropertySources) {
            if (propertySource instanceof OriginTrackedMapPropertySource) {
                mutablePropertySources.replace(propertySource.getName(),
                        new PropertySourceWrapper(propertySource
                                , new AesEncryptor(PrivateKeyFinder.getSecretKey(secretKey))
                                , new EncryptionWrapperDetector("$ENC{", "}"))
                );
            }
        }
    }

    @Override
    public int getOrder() {
        return Ordered. LOWEST_PRECEDENCE - 100;
    }
}

  • Define a PropertySource wrapper class
    • PropertySource has only one method public Object getProperty(String name), you only need to implement this method, if it is an encrypted configuration, decrypt it
public class PropertySourceWrapper<T> extends PropertySource<T> {
    private final String prefix = "$ENC{";
    private final String suffix = "}";

    private final Encryptor encryptor;

    private final PropertySource<T> originalPropertySource;
    private final EncryptionWrapperDetector detector;


    public PropertySourceWrapper(PropertySource<T> originalPropertySource, Encryptor encryptor, EncryptionWrapperDetector detector) {
        super(originalPropertySource. getName(), originalPropertySource. getSource());
        this. originalPropertySource = originalPropertySource;
        this.encryptor = encryptor;
        this. detector = detector;
    }

    @Override
    public Object getProperty(String name) {
        if (originalPropertySource. containsProperty(name)) {
            Object value = originalPropertySource.getProperty(name);
            if (value != null) {
                String property = value. toString();
                if (detector. detected(property)) {
                    return encryptor.decrypt(detector.unWrapper(property));
                }
            }
        }
        return originalPropertySource.getProperty(name);
    }
}
  • Define an encryption and decryption helper class EncryptionWrapperDetector
    • Judging whether it is an encrypted attribute according to the prefix and suffix
    • Wrapping encrypted properties
    • Unwrap encrypted attributes
public class EncryptionWrapperDetector {
    private final String prefix;

    private final String suffix;

    public EncryptionWrapperDetector(String prefix, String suffix) {
        this. prefix = prefix;
        this.suffix = suffix;
    }

    public boolean detected(String property) {
        return property != null & amp; & amp; property.startsWith(prefix) & amp; & amp; property.endsWith(suffix);
    }

    public String wrapper(String property) {
        return prefix + property + suffix;
    }

    public String unWrapper(String property) {
        return property.substring(prefix.length(), property.length() - suffix.length());
    }
}
  • Define an encryption and decryption class
    • Encrypt configuration files
    • Decrypt the configuration asking price
public class AesEncryptor implements Encryptor {

    private final byte[] secretKey;
    private final byte[] iv = new byte[16];

    public AesEncryptor(byte[] secretKey) {
        this.secretKey = secretKey;
        System.arraycopy(secretKey, 0, iv, 0, 16);
    }

    @Override
    public String encrypt(String message) {
        return Base64.getEncoder().encodeToString(_AesUtils.encrypt(secretKey, iv, message.getBytes()));
    }

    @Override
    public String decrypt(String message) {
        return new String(_AesUtils.decrypt(secretKey, iv, Base64.getDecoder().decode(message)));
    }
}
  • key encrypted storage
    • The key is encrypted by asymmetric encryption, and the key encrypted with the public key can be directly written in the configuration file
    • When decrypting, first use the built-in private key to decrypt to obtain the original encryption key
    • Attention to detail
      • When the private key is stored, it can be encrypted again
      • The private key can be placed under the META-INF path and obtained through Classloader
public class PrivateKeyFinder {
    private static final String PRIVATE_KEY_RESOURCE_LOCATION = "META-INF/configuration.crypto.private-key";
    private static final String PUBLIC_KEY_RESOURCE_LOCATION = "META-INF/configuration.crypto.public-key";
    private final byte[] keyInfo = new byte[]{
            (byte) 0xD0, (byte) 0x20, (byte) 0xDA, (byte) 0x92, (byte) 0xC8, (byte) 0x0B, (byte) 0x6D, (byte) 0x57,
            (byte) 0x48, (byte) 0x7B, (byte) 0x15, (byte) 0x3A, (byte) 0x44, (byte) 0xA0, (byte) 0x98, (byte) 0xC2,
            (byte) 0xF1, (byte) 0x6F, (byte) 0xB6, (byte) 0x09, (byte) 0x2F, (byte) 0x6D, (byte) 0x69, (byte) 0xFB,
            (byte) 0x2D, (byte) 0x02, (byte) 0x00, (byte) 0xCB, (byte) 0xBE, (byte) 0x48, (byte) 0xDD, (byte) 0xD5,
            (byte) 0x90, (byte) 0xC2, (byte) 0x95, (byte) 0x98, (byte) 0x60, (byte) 0x59, (byte) 0x24, (byte) 0xE2,
            (byte) 0xB7, (byte) 0x84, (byte) 0x12, (byte) 0x5D, (byte) 0xB9, (byte) 0xC1, (byte) 0x19, (byte) 0xFF,
            (byte) 0x4F, (byte) 0x01, (byte) 0xB9, (byte) 0xC5, (byte) 0xD8, (byte) 0xD2, (byte) 0x99, (byte) 0xEE,
            (byte) 0xAA, (byte) 0x0D, (byte) 0x59, (byte) 0xF8, (byte) 0x37, (byte) 0x49, (byte) 0x91, (byte) 0xAB
    };

    static byte[] getSecretKey(String encKey) {
        byte[] key = loadPrivateKey();
        return RsaUtils.decrypt(Base64.getDecoder().decode(encKey), new PrivateKeyFinder().decrypt(Base64.getDecoder().decode(key)));
    }

    static String generateSecretKey() {
        return Base64.getEncoder().encodeToString(RsaUtils.encrypt(new SecureRandom().generateSeed(16), Base64.getDecoder().decode(loadPublicKey())));
    }

    static String generateSecretKeyWith256() {
        return Base64.getEncoder().encodeToString(RsaUtils.encrypt(new SecureRandom().generateSeed(32), Base64.getDecoder().decode(loadPublicKey())));
    }

    @SneakyThrows
    static byte[] loadPrivateKey() {
        return loadResource(PRIVATE_KEY_RESOURCE_LOCATION);
    }

    @SneakyThrows
    static byte[] loadPublicKey() {
        return loadResource(PUBLIC_KEY_RESOURCE_LOCATION);
    }

    @SneakyThrows
    private static byte[] loadResource(String location) {
        // just lookup from current jar path
        ClassLoader classLoader = new URLClassLoader(new URL[]{PrivateKeyFinder. class. getProtectionDomain(). getCodeSource(). getLocation()}, null);
// classLoader = PrivateKeyFinder. class. getClassLoader();
        Enumeration<URL> enumeration = classLoader. getResources(location);

        // should only find one
        while (enumeration. hasMoreElements()) {
            URL url = enumeration. nextElement();
            UrlResource resource = new UrlResource(url);
            return FileCopyUtils.copyToByteArray(resource.getInputStream());
        }

        return null;
    }

    private final String CIPHER_ALGORITHM = "AES/CBC/NoPadding";
    private final String KEY_TYPE = "AES";

    @SneakyThrows
    public byte[] encrypt(byte[] data) {
        byte[] key = new byte[32];
        byte[] iv = new byte[16];
        System.arraycopy(keyInfo, 0, key, 0, 32);
        System.arraycopy(keyInfo, 32, iv, 0, 16);
        Cipher cipher = Cipher. getInstance(CIPHER_ALGORITHM);
        cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(key, KEY_TYPE), new IvParameterSpec(iv));
        return cipher. doFinal(data);
    }

    @SneakyThrows
    public byte[] decrypt(byte[] data) {
        byte[] key = new byte[32];
        byte[] iv = new byte[16];
        System.arraycopy(keyInfo, 0, key, 0, 32);
        System.arraycopy(keyInfo, 32, iv, 0, 16);

        Cipher cipher = Cipher. getInstance(CIPHER_ALGORITHM);
        cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(key, KEY_TYPE), new IvParameterSpec(iv));
        return cipher. doFinal(data);
    }
}

In summary, the encryption and decryption of sensitive configuration files can be realized, and the security of the encryption key can be guaranteed at the same time.