Fading Coder

One Final Commit for the Last Sprint

Home > Tech > Content

Resolving Encrypted Values in @ConfigurationProperties Injection Despite Environment Variable Decryption

Tech 3

In a microservice application, a configuration property test.container-name is defined in application.properties as:

test.container-name=Tomcat

A Java class TestConfigProperty uses @ConfigurationProperties to inject this property:

@ConfigurationProperties(prefix = "test")
@Component
public class TestConfigProperty {
    private String containerName;

    public String getContainerName() {
        return containerName;
    }

    public void setContainerName(String containerName) {
        this.containerName = containerName;
    }
}

To secure sensitive data, test.container-name is encrypted and referenced via an environment varible:

test.container-name=${TEST_CONTAINER_NAME}

The environment variable holds an encrypted value (e.g., Base64-encoded). A custom DecryptEnvironmentPostProcessor extends EnvironmentPostProcessor to decrypt values prefixed with ENC_ at startup:

public class DecryptEnvironmentPostProcessor implements EnvironmentPostProcessor, Ordered {
    private static final String DECRYPTED_SOURCE_NAME = "decryptedSystemEnvironment";

    @Override
    public void postProcessEnvironment(ConfigurableEnvironment env, SpringApplication app) {
        String sysEnvName = StandardEnvironment.SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME;
        MapPropertySource sysEnvSource = (MapPropertySource) env.getPropertySources().get(sysEnvName);
        Map<String, Object> decryptedValues = new HashMap<>();
        if (sysEnvSource == null) {
            return;
        }
        sysEnvSource.getSource().forEach((key, value) -> {
            if (value instanceof String strVal) {
                if (StringUtils.isNotEmpty(strVal) && strVal.startsWith("ENC_")) {
                    String decrypted = new String(Base64.getDecoder().decode(strVal.substring(4)));
                    decryptedValues.put(key, decrypted);
                }
            }
        });

        if (!decryptedValues.isEmpty()) {
            MapPropertySource decryptedSource = new MapPropertySource(DECRYPTED_SOURCE_NAME, decryptedValues);
            env.getPropertySources().addBefore(sysEnvName, decryptedSource);
        }
    }

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

Despite decryption, TestConfigProperty.containerName receives the encrypted value. Debugging shows the decrypted PropertySource precedes the encrypted one, yet injection yields the encrypted value.

Root Cause Analysis

Property binding for @ConfigurationProperties occurs in ConfigurationPropertiesBindingPostProcessor.postProcessBeforeInitialization(), which delegates to ConfigurationPropertiesBinder.bind(). This calls Binder.bind(), passing the prefix test to create a ConfigurationPropertyName. The process recursively binds properties via bindObject() and bindDataObject().

For JavaBeans, JavaBeanBinder.bind() retrieves all BeanProperty instances. The property containerName is normalized to dash format (container-name). The binder appends this to the prefix, forming test.container-name, and recursively searches for this property in ConfigurationPropertySource instances.

SpringIterableConfigurationPropertySource adapts PropertySource objects. The decrypted PropertySource (MapPropertySource) is placed before the encrypted SystemEnvironmentPropertySource. However, SystemEnvironmentPropertySource uses SystemEnvironmentPropertyMapper, which maps test.container-name to TEST_CONTAINERNAME and TEST_CONTAINER_NAME. The encrypted source contains TEST_CONTAINER_NAME=ENC_VG9tY2F0, while the decrypted MapPropertySource only has TEST_CONTAINER_NAME=Tomcat and lacks the SystemEnvironmentPropertyMapper. Thus, when searching for test.container-name:

  1. The decrypted MapPropertySource (with DefaultPropertyMapper) maps test.container-name to test.container-name, which doesn't match TEST_CONTAINER_NAME.
  2. The encrypted SystemEnvironmentPropertySource (with SystemEnvironmentPropertyMapper) maps test.container-name to TEST_CONTAINER_NAME, retrieving the encrypted value ENC_VG9tY2F0.

Solution

Modify DecryptEnvironmentPostProcessor to use SystemEnvironmentPropertySource for the decrypted properties, ensuring it includes SystemEnvironmentPropertyMapper:

@Override
public void postProcessEnvironment(ConfigurableEnvironment env, SpringApplication app) {
    String sysEnvName = StandardEnvironment.SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME;
    MapPropertySource sysEnvSource = (MapPropertySource) env.getPropertySources().get(sysEnvName);
    Map<String, Object> decryptedValues = new HashMap<>();
    if (sysEnvSource == null) {
        return;
    }
    sysEnvSource.getSource().forEach((key, value) -> {
        if (value instanceof String strVal) {
            if (StringUtils.isNotEmpty(strVal) && strVal.startsWith("ENC_")) {
                String decrypted = new String(Base64.getDecoder().decode(strVal.substring(4)));
                decryptedValues.put(key, decrypted);
            }
        }
    });

    if (!decryptedValues.isEmpty()) {
        // Use SystemEnvironmentPropertySource instead of MapPropertySource
        env.getPropertySources().addBefore(sysEnvName,
                new SystemEnvironmentPropertySource(DECRYPTED_SOURCE_NAME, decryptedValues));
    }
}

This ensures the decrypted source uses the same property mapping as the original system environment, allowing test.container-name to correctly resolve to the decrypted value.

Related Articles

Understanding Strong and Weak References in Java

Strong References Strong reference are the most prevalent type of object referencing in Java. When an object has a strong reference pointing to it, the garbage collector will not reclaim its memory. F...

Implement Image Upload Functionality for Django Integrated TinyMCE Editor

Django’s Admin panel is highly user-friendly, and pairing it with TinyMCE, an effective rich text editor, simplifies content management significantly. Combining the two is particular useful for bloggi...

SBUS Signal Analysis and Communication Implementation Using STM32 with Fus Remote Controller

Overview In a recent project, I utilized the SBUS protocol with the Fus remote controller to control a vehicle's basic operations, including movement, lights, and mode switching. This article is aimed...

Leave a Comment

Anonymous

◎Feel free to join the discussion and share your thoughts.