diff --git a/spring-cloud-context/src/main/java/org/springframework/cloud/autoconfigure/EncryptionBootstrapAutoConfiguration.java b/spring-cloud-context/src/main/java/org/springframework/cloud/autoconfigure/EncryptionBootstrapAutoConfiguration.java new file mode 100644 index 00000000..478f4de4 --- /dev/null +++ b/spring-cloud-context/src/main/java/org/springframework/cloud/autoconfigure/EncryptionBootstrapAutoConfiguration.java @@ -0,0 +1,170 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.autoconfigure; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.condition.ConditionOutcome; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.autoconfigure.condition.SpringBootCondition; +import org.springframework.boot.context.properties.ConfigurationPropertiesBinding; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.cloud.bootstrap.TextEncryptorConfigurationPropertiesBindHandlerAdvisor; +import org.springframework.cloud.bootstrap.encrypt.KeyProperties; +import org.springframework.cloud.bootstrap.encrypt.KeyProperties.KeyStore; +import org.springframework.cloud.bootstrap.encrypt.RsaProperties; +import org.springframework.cloud.context.encrypt.EncryptorFactory; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.ConditionContext; +import org.springframework.context.annotation.Conditional; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.env.Environment; +import org.springframework.core.type.AnnotatedTypeMetadata; +import org.springframework.security.crypto.encrypt.TextEncryptor; +import org.springframework.security.rsa.crypto.KeyStoreKeyFactory; +import org.springframework.security.rsa.crypto.RsaSecretEncryptor; +import org.springframework.util.StringUtils; + +/** + * @author Dave Syer + * + */ +@Configuration(proxyBeanMethods = false) +@ConditionalOnClass({ TextEncryptor.class }) +@ConditionalOnProperty(value = "spring.config.use-legacy-processing", havingValue = "false") +public class EncryptionBootstrapAutoConfiguration { + + @Autowired(required = false) + private TextEncryptor encryptor; + + @Autowired + private KeyProperties key; + + @Bean + @ConfigurationPropertiesBinding + TextEncryptorConfigurationPropertiesBindHandlerAdvisor textEncryptorConfigurationPropertiesBindHandlerAdvisor(ApplicationContext applicationContext) { + return new TextEncryptorConfigurationPropertiesBindHandlerAdvisor(applicationContext); + } + + @Configuration(proxyBeanMethods = false) + @Conditional(KeyCondition.class) + @ConditionalOnClass(RsaSecretEncryptor.class) + @EnableConfigurationProperties({ RsaProperties.class }) + protected static class RsaEncryptionConfiguration { + + @Autowired + private KeyProperties key; + + @Autowired + private RsaProperties rsaProperties; + + @Bean + @ConditionalOnMissingBean(TextEncryptor.class) + @ConfigurationPropertiesBinding + public TextEncryptor textEncryptor() { + KeyStore keyStore = this.key.getKeyStore(); + if (keyStore.getLocation() != null) { + if (keyStore.getLocation().exists()) { + return new RsaSecretEncryptor( + new KeyStoreKeyFactory(keyStore.getLocation(), keyStore.getPassword().toCharArray()) + .getKeyPair(keyStore.getAlias(), keyStore.getSecret().toCharArray()), + this.rsaProperties.getAlgorithm(), this.rsaProperties.getSalt(), + this.rsaProperties.isStrong()); + } + + throw new IllegalStateException("Invalid keystore location"); + } + + return new EncryptorFactory(this.key.getSalt()).create(this.key.getKey()); + } + + } + + @Configuration(proxyBeanMethods = false) + @Conditional(KeyCondition.class) + @ConditionalOnMissingClass("org.springframework.security.rsa.crypto.RsaSecretEncryptor") + protected static class VanillaEncryptionConfiguration { + + @Autowired + private KeyProperties key; + + @Bean + @ConditionalOnMissingBean(TextEncryptor.class) + @ConfigurationPropertiesBinding + public TextEncryptor textEncryptor() { + return new EncryptorFactory(this.key.getSalt()).create(this.key.getKey()); + } + + } + + /** + * A Spring Boot condition for key encryption. + */ + public static class KeyCondition extends SpringBootCondition { + + @Override + public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata metadata) { + Environment environment = context.getEnvironment(); + if (hasProperty(environment, "encrypt.key-store.location")) { + if (hasProperty(environment, "encrypt.key-store.password")) { + return ConditionOutcome.match("Keystore found in Environment"); + } + return ConditionOutcome.noMatch("Keystore found but no password in Environment"); + } + else if (hasProperty(environment, "encrypt.key")) { + return ConditionOutcome.match("Key found in Environment"); + } + return ConditionOutcome.noMatch("Keystore nor key found in Environment"); + } + + private boolean hasProperty(Environment environment, String key) { + String value = environment.getProperty(key); + if (value == null) { + return false; + } + return StringUtils.hasText(environment.resolvePlaceholders(value)); + } + + } + + /** + * TextEncryptor that just fails, so that users don't get a false sense of security + * adding ciphers to config files and not getting them decrypted. + * + * @author Dave Syer + * + */ + protected static class FailsafeTextEncryptor implements TextEncryptor { + + @Override + public String encrypt(String text) { + throw new UnsupportedOperationException( + "No encryption for FailsafeTextEncryptor. Did you configure the keystore correctly?"); + } + + @Override + public String decrypt(String encryptedText) { + throw new UnsupportedOperationException( + "No decryption for FailsafeTextEncryptor. Did you configure the keystore correctly?"); + } + + } + +} diff --git a/spring-cloud-context/src/main/java/org/springframework/cloud/bootstrap/TextEncryptorConfigBootstrapper.java b/spring-cloud-context/src/main/java/org/springframework/cloud/bootstrap/TextEncryptorConfigBootstrapper.java new file mode 100644 index 00000000..7b826af2 --- /dev/null +++ b/spring-cloud-context/src/main/java/org/springframework/cloud/bootstrap/TextEncryptorConfigBootstrapper.java @@ -0,0 +1,107 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.bootstrap; + +import org.springframework.boot.BootstrapRegistry; +import org.springframework.boot.Bootstrapper; +import org.springframework.boot.context.properties.bind.Binder; +import org.springframework.cloud.bootstrap.encrypt.KeyProperties; +import org.springframework.cloud.bootstrap.encrypt.RsaProperties; +import org.springframework.core.env.Environment; +import org.springframework.security.crypto.encrypt.TextEncryptor; +import org.springframework.util.ClassUtils; + +/** + * Bootstrapper + * + * @author Marcin Grzejszczak + * @since 3.0.0 + */ +public class TextEncryptorConfigBootstrapper implements Bootstrapper { + + @Override + public void intitialize(BootstrapRegistry registry) { + if (!ClassUtils.isPresent("org.springframework.security.crypto.encrypt.TextEncryptor", null)) { + return; + } + + registry.registerIfAbsent(KeyProperties.class, + context -> context.get(Binder.class) + .bind("encrypt", KeyProperties.class) + .orElseGet(KeyProperties::new)); + registry.registerIfAbsent(RsaProperties.class, + context -> context.get(Binder.class) + .bind("encrypt.rsa", RsaProperties.class) + .orElseGet(RsaProperties::new)); + + + /*registry.registerIfAbsent(TextEncryptorConfigurationPropertiesBindHandlerAdvisor.class, context -> { + if (isLegacyProcessingEnabled(context.get(Binder.class))) { + return null; + } + return new TextEncryptorConfigurationPropertiesBindHandlerAdvisor(context.get(TextEncryptor.class)); + });*/ + + // promote beans to context + registry.addCloseListener(event -> { + if (isLegacyProcessingEnabled(event.getApplicationContext().getEnvironment())) { + return; + } + KeyProperties keyProperties = event.getBootstrapContext().get(KeyProperties.class); + if (keyProperties != null) { + event.getApplicationContext().getBeanFactory().registerSingleton("keyProperties", + keyProperties); + } + RsaProperties rsaProperties = event.getBootstrapContext().get(RsaProperties.class); + if (rsaProperties != null) { + event.getApplicationContext().getBeanFactory().registerSingleton("rsaProperties", + rsaProperties); + } + }); + } + + private boolean isLegacyProcessingEnabled(Binder binder) { + return binder.bind("spring.config.use-legacy-processing", Boolean.class).orElse(false); + } + + private boolean isLegacyProcessingEnabled(Environment environment) { + return environment.getProperty("spring.config.use-legacy-processing", Boolean.class, false); + } + + /** + * TextEncryptor that just fails, so that users don't get a false sense of security + * adding ciphers to config files and not getting them decrypted. + * + * @author Dave Syer + * + */ + protected static class FailsafeTextEncryptor implements TextEncryptor { + + @Override + public String encrypt(String text) { + throw new UnsupportedOperationException( + "No encryption for FailsafeTextEncryptor. Did you configure the keystore correctly?"); + } + + @Override + public String decrypt(String encryptedText) { + throw new UnsupportedOperationException( + "No decryption for FailsafeTextEncryptor. Did you configure the keystore correctly?"); + } + + } +} diff --git a/spring-cloud-context/src/main/java/org/springframework/cloud/bootstrap/TextEncryptorConfigurationPropertiesBindHandlerAdvisor.java b/spring-cloud-context/src/main/java/org/springframework/cloud/bootstrap/TextEncryptorConfigurationPropertiesBindHandlerAdvisor.java new file mode 100644 index 00000000..f4f9c729 --- /dev/null +++ b/spring-cloud-context/src/main/java/org/springframework/cloud/bootstrap/TextEncryptorConfigurationPropertiesBindHandlerAdvisor.java @@ -0,0 +1,150 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.bootstrap; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.boot.context.properties.ConfigurationPropertiesBindHandlerAdvisor; +import org.springframework.boot.context.properties.bind.AbstractBindHandler; +import org.springframework.boot.context.properties.bind.BindContext; +import org.springframework.boot.context.properties.bind.BindHandler; +import org.springframework.boot.context.properties.bind.Bindable; +import org.springframework.boot.context.properties.source.ConfigurationPropertyName; +import org.springframework.cloud.bootstrap.encrypt.KeyProperties; +import org.springframework.context.ApplicationContext; +import org.springframework.security.crypto.encrypt.TextEncryptor; + +public class TextEncryptorConfigurationPropertiesBindHandlerAdvisor implements ConfigurationPropertiesBindHandlerAdvisor { + + private final ApplicationContext applicationContext; + + public TextEncryptorConfigurationPropertiesBindHandlerAdvisor(ApplicationContext applicationContext) { + this.applicationContext = applicationContext; + } + + @Override + public BindHandler apply(BindHandler bindHandler) { + return wrappedBindHandler(bindHandler); + } + + private BindHandler wrappedBindHandler(BindHandler handler) { + KeyProperties keyProperties = this.applicationContext.getBean(KeyProperties.class); + TextEncryptorBindHandler textEncryptorBindHandler = new TextEncryptorBindHandler(this.applicationContext.getBeanProvider(TextEncryptor.class).getIfAvailable(FailsafeTextEncryptor::new), keyProperties); + return new BindHandler() { + @Override + public Bindable onStart(ConfigurationPropertyName name, Bindable target, BindContext context) { + return handler.onStart(name, target, context); + } + + @Override + public Object onSuccess(ConfigurationPropertyName name, Bindable target, BindContext context, Object result) { + Object defaultHandlerResult = handler.onSuccess(name, target, context, result); + return textEncryptorBindHandler.onSuccess(name, target, context, defaultHandlerResult); + } + + @Override + public Object onCreate(ConfigurationPropertyName name, Bindable target, BindContext context, Object result) { + return handler.onCreate(name, target, context, result); + } + + @Override + public Object onFailure(ConfigurationPropertyName name, Bindable target, BindContext context, Exception error) throws Exception { + return handler.onFailure(name, target, context, error); + } + + @Override + public void onFinish(ConfigurationPropertyName name, Bindable target, BindContext context, Object result) throws Exception { + handler.onFinish(name, target, context, result); + } + }; + } + + /** + * TextEncryptor that just fails, so that users don't get a false sense of security + * adding ciphers to config files and not getting them decrypted. + * + * @author Dave Syer + * + */ + protected static class FailsafeTextEncryptor implements TextEncryptor { + + @Override + public String encrypt(String text) { + throw new UnsupportedOperationException( + "No encryption for FailsafeTextEncryptor. Did you configure the keystore correctly?"); + } + + @Override + public String decrypt(String encryptedText) { + throw new UnsupportedOperationException( + "No decryption for FailsafeTextEncryptor. Did you configure the keystore correctly?"); + } + + } + + protected static class TextEncryptorBindHandler extends AbstractBindHandler { + + private static final Log logger = LogFactory.getLog(TextEncryptorBindHandler.class); + + /** + * Prefix indicating an encrypted value. + */ + protected static final String ENCRYPTED_PROPERTY_PREFIX = "{cipher}"; + + private final TextEncryptor textEncryptor; + + private final KeyProperties keyProperties; + + public TextEncryptorBindHandler(TextEncryptor textEncryptor, KeyProperties keyProperties) { + this.textEncryptor = textEncryptor; + this.keyProperties = keyProperties; + } + + @Override + public Object onSuccess(ConfigurationPropertyName name, Bindable target, BindContext context, Object result) { + if (result instanceof String) { + return decrypt(name.toString(), (String) result); + } + return result; + } + + private String decrypt(String key, String original) { + String value = original.substring(ENCRYPTED_PROPERTY_PREFIX.length()); + try { + value = this.textEncryptor.decrypt(value); + if (logger.isDebugEnabled()) { + logger.debug("Decrypted: key=" + key); + } + return value; + } + catch (Exception e) { + String message = "Cannot decrypt: key=" + key; + if (logger.isDebugEnabled()) { + logger.warn(message, e); + } + else { + logger.warn(message); + } + if (this.keyProperties.isFailOnError()) { + throw new IllegalStateException(message, e); + } + return ""; + } + } + } +} diff --git a/spring-cloud-context/src/main/resources/META-INF/spring.factories b/spring-cloud-context/src/main/resources/META-INF/spring.factories index 0fde0d1b..15e394ec 100644 --- a/spring-cloud-context/src/main/resources/META-INF/spring.factories +++ b/spring-cloud-context/src/main/resources/META-INF/spring.factories @@ -4,7 +4,8 @@ org.springframework.cloud.autoconfigure.ConfigurationPropertiesRebinderAutoConfi org.springframework.cloud.autoconfigure.LifecycleMvcEndpointAutoConfiguration,\ org.springframework.cloud.autoconfigure.RefreshAutoConfiguration,\ org.springframework.cloud.autoconfigure.RefreshEndpointAutoConfiguration,\ -org.springframework.cloud.autoconfigure.WritableEnvironmentEndpointAutoConfiguration +org.springframework.cloud.autoconfigure.WritableEnvironmentEndpointAutoConfiguration,\ +org.springframework.cloud.autoconfigure.EncryptionBootstrapAutoConfiguration # Application Listeners org.springframework.context.ApplicationListener=\ org.springframework.cloud.bootstrap.BootstrapApplicationListener,\ @@ -17,3 +18,5 @@ org.springframework.cloud.bootstrap.encrypt.EncryptionBootstrapConfiguration,\ org.springframework.cloud.autoconfigure.ConfigurationPropertiesRebinderAutoConfiguration,\ org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration,\ org.springframework.cloud.util.random.CachedRandomPropertySourceAutoConfiguration +org.springframework.boot.Bootstrapper=\ +org.springframework.cloud.bootstrap.TextEncryptorConfigBootstrapper \ No newline at end of file diff --git a/spring-cloud-context/src/test/java/org/springframework/cloud/bootstrap/encrypt/EncryptionIntegrationTests.java b/spring-cloud-context/src/test/java/org/springframework/cloud/bootstrap/encrypt/EncryptionIntegrationTests.java index 9def18ec..55ba62f7 100644 --- a/spring-cloud-context/src/test/java/org/springframework/cloud/bootstrap/encrypt/EncryptionIntegrationTests.java +++ b/spring-cloud-context/src/test/java/org/springframework/cloud/bootstrap/encrypt/EncryptionIntegrationTests.java @@ -19,6 +19,7 @@ package org.springframework.cloud.bootstrap.encrypt; import org.junit.Test; import org.springframework.boot.WebApplicationType; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.builder.SpringApplicationBuilder; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.boot.context.properties.EnableConfigurationProperties; @@ -30,7 +31,7 @@ import static org.assertj.core.api.BDDAssertions.then; public class EncryptionIntegrationTests { @Test - public void symmetricPropertyValues() { + public void legacySymmetricPropertyValues() { ConfigurableApplicationContext context = new SpringApplicationBuilder(TestConfiguration.class) .web(WebApplicationType.NONE).properties("spring.config.use-legacy-processing=true", "encrypt.key:pie", "foo.password:{cipher}bf29452295df354e6153c5b31b03ef23c70e55fba24299aa85c63438f1c43c95") @@ -39,7 +40,7 @@ public class EncryptionIntegrationTests { } @Test - public void symmetricConfigurationProperties() { + public void legacySymmetricConfigurationProperties() { ConfigurableApplicationContext context = new SpringApplicationBuilder(TestConfiguration.class) .web(WebApplicationType.NONE).properties("spring.config.use-legacy-processing=true", "encrypt.key:pie", "foo.password:{cipher}bf29452295df354e6153c5b31b03ef23c70e55fba24299aa85c63438f1c43c95") @@ -47,12 +48,55 @@ public class EncryptionIntegrationTests { then(context.getBean(PasswordProperties.class).getPassword()).isEqualTo("test"); } + @Test + public void propSymmetricPropertyValues() { + ConfigurableApplicationContext context = new SpringApplicationBuilder(TestConfiguration.class) + .web(WebApplicationType.NONE).properties("spring.cloud.bootstrap.enabled=true", "encrypt.key:pie", + "foo.password:{cipher}bf29452295df354e6153c5b31b03ef23c70e55fba24299aa85c63438f1c43c95") + .run(); + then(context.getEnvironment().getProperty("foo.password")).isEqualTo("test"); + } + + @Test + public void propSymmetricConfigurationProperties() { + ConfigurableApplicationContext context = new SpringApplicationBuilder(TestConfiguration.class) + .web(WebApplicationType.NONE).properties("spring.cloud.bootstrap.enabled=true", "encrypt.key:pie", + "foo.password:{cipher}bf29452295df354e6153c5b31b03ef23c70e55fba24299aa85c63438f1c43c95") + .run(); + then(context.getBean(PasswordProperties.class).getPassword()).isEqualTo("test"); + } + + @Test + public void symmetricPropertyValues() { + ConfigurableApplicationContext context = new SpringApplicationBuilder(TestAutoConfiguration.class) + .web(WebApplicationType.NONE).properties("spring.config.use-legacy-processing=false", "encrypt.key:pie", + "foo.password:{cipher}bf29452295df354e6153c5b31b03ef23c70e55fba24299aa85c63438f1c43c95") + .run(); + then(context.getEnvironment().getProperty("foo.password")).isEqualTo("test"); + } + + @Test + public void symmetricConfigurationProperties() { + ConfigurableApplicationContext context = new SpringApplicationBuilder(TestAutoConfiguration.class) + .web(WebApplicationType.NONE).properties("spring.config.use-legacy-processing=false", "encrypt.key:pie", + "foo.password:{cipher}bf29452295df354e6153c5b31b03ef23c70e55fba24299aa85c63438f1c43c95") + .run(); + then(context.getBean(PasswordProperties.class).getPassword()).isEqualTo("test"); + } + @Configuration(proxyBeanMethods = false) @EnableConfigurationProperties(PasswordProperties.class) protected static class TestConfiguration { } + @Configuration(proxyBeanMethods = false) + @EnableAutoConfiguration + @EnableConfigurationProperties(PasswordProperties.class) + protected static class TestAutoConfiguration { + + } + @ConfigurationProperties("foo") protected static class PasswordProperties {