Creates TextEncryptorBindHandler for Binder decryption.

It is registered in TextEncryptorConfigBootstrapper for later use in
other ConfigData implementations.
This commit is contained in:
Marcin Grzejszczak
2020-12-18 08:50:02 +01:00
committed by spencergibb
parent 1f0f750707
commit 7211cdd2e8
5 changed files with 477 additions and 3 deletions

View File

@@ -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?");
}
}
}

View File

@@ -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?");
}
}
}

View File

@@ -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 <T> Bindable<T> onStart(ConfigurationPropertyName name, Bindable<T> 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 "";
}
}
}
}

View File

@@ -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

View File

@@ -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 {