Merge pull request #872 from spring-cloud/rebinder

Creates TextEncryptorBindHandler for Binder decryption.

It is registered in TextEncryptorConfigBootstrapper for later use in
other ConfigData implementations.

Creates DecryptEnvironmentPostProcessor. 

This is used if bootstrap and legacy processing are not enabled.

EnvironmentDecryptApplicationInitializer is only is if bootstrap and legacy processing are enabled.

Fixes gh-815

@spencergibb
This commit is contained in:
Spencer Gibb
2020-12-20 11:16:18 -05:00
committed by GitHub
12 changed files with 634 additions and 173 deletions

View File

@@ -0,0 +1,86 @@
/*
* Copyright 2013-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.bind.AbstractBindHandler;
import org.springframework.boot.context.properties.bind.BindContext;
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.security.crypto.encrypt.TextEncryptor;
/**
* BindHandler that uses a TextEncryptor to decrypt text if properly prefixed with
* {cipher}.
*
* @author Marcin Grzejszczak
* @since 3.0.0
*/
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;
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 && ((String) result).startsWith(ENCRYPTED_PROPERTY_PREFIX)) {
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

@@ -0,0 +1,168 @@
/*
* 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.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.boot.BootstrapContext;
import org.springframework.boot.BootstrapRegistry;
import org.springframework.boot.Bootstrapper;
import org.springframework.boot.context.properties.bind.BindHandler;
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.cloud.context.encrypt.EncryptorFactory;
import org.springframework.core.env.Environment;
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.ClassUtils;
import org.springframework.util.StringUtils;
/**
* Bootstrapper.
*
* @author Marcin Grzejszczak
* @since 3.0.0
*/
public class TextEncryptorConfigBootstrapper implements Bootstrapper {
private static final boolean RSA_IS_PRESENT = ClassUtils
.isPresent("org.springframework.security.rsa.crypto.RsaSecretEncryptor", null);
@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(KeyProperties.PREFIX, KeyProperties.class).orElseGet(KeyProperties::new));
if (RSA_IS_PRESENT) {
registry.registerIfAbsent(RsaProperties.class, context -> context.get(Binder.class)
.bind(RsaProperties.PREFIX, RsaProperties.class).orElseGet(RsaProperties::new));
}
registry.registerIfAbsent(TextEncryptor.class, context -> {
KeyProperties keyProperties = context.get(KeyProperties.class);
if (keysConfigured(keyProperties)) {
if (RSA_IS_PRESENT) {
RsaProperties rsaProperties = context.get(RsaProperties.class);
return rsaTextEncryptor(keyProperties, rsaProperties);
}
return new EncryptorFactory(keyProperties.getSalt()).create(keyProperties.getKey());
}
// no keys configured
return new FailsafeTextEncryptor();
});
registry.registerIfAbsent(BindHandler.class, context -> {
TextEncryptor textEncryptor = context.get(TextEncryptor.class);
if (textEncryptor != null) {
KeyProperties keyProperties = context.get(KeyProperties.class);
return new TextEncryptorBindHandler(textEncryptor, keyProperties);
}
return null;
});
// promote beans to context
registry.addCloseListener(event -> {
if (isLegacyBootstrap(event.getApplicationContext().getEnvironment())) {
return;
}
BootstrapContext bootstrapContext = event.getBootstrapContext();
KeyProperties keyProperties = bootstrapContext.get(KeyProperties.class);
ConfigurableListableBeanFactory beanFactory = event.getApplicationContext().getBeanFactory();
if (keyProperties != null) {
beanFactory.registerSingleton("keyProperties", keyProperties);
}
if (RSA_IS_PRESENT) {
RsaProperties rsaProperties = bootstrapContext.get(RsaProperties.class);
if (rsaProperties != null) {
beanFactory.registerSingleton("rsaProperties", rsaProperties);
}
}
TextEncryptor textEncryptor = bootstrapContext.get(TextEncryptor.class);
if (textEncryptor != null) {
beanFactory.registerSingleton("textEncryptor", textEncryptor);
}
});
}
public static TextEncryptor rsaTextEncryptor(KeyProperties keyProperties, RsaProperties rsaProperties) {
KeyProperties.KeyStore keyStore = keyProperties.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()),
rsaProperties.getAlgorithm(), rsaProperties.getSalt(), rsaProperties.isStrong());
}
throw new IllegalStateException("Invalid keystore location");
}
return new EncryptorFactory(keyProperties.getSalt()).create(keyProperties.getKey());
}
public static boolean keysConfigured(KeyProperties properties) {
if (hasProperty(properties.getKeyStore().getLocation())) {
if (hasProperty(properties.getKeyStore().getPassword())) {
return true;
}
return false;
}
else if (hasProperty(properties.getKey())) {
return true;
}
return false;
}
static boolean hasProperty(Object value) {
if (value instanceof String) {
return StringUtils.hasText((String) value);
}
return value != null;
}
static boolean isLegacyBootstrap(Environment environment) {
boolean isLegacy = environment.getProperty("spring.config.use-legacy-processing", Boolean.class, false);
boolean isBootstrapEnabled = environment.getProperty("spring.cloud.bootstrap.enabled", Boolean.class, false);
return isLegacy || isBootstrapEnabled;
}
/**
* 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
*
*/
public 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,165 @@
/*
* Copyright 2013-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.encrypt;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Pattern;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.core.env.CompositePropertySource;
import org.springframework.core.env.EnumerablePropertySource;
import org.springframework.core.env.PropertySource;
import org.springframework.core.env.PropertySources;
import org.springframework.security.crypto.encrypt.TextEncryptor;
/**
* Abstract class that handles decrypting and merging of PropertySources.
*/
public class AbstractEnvironmentDecrypt {
private static final Pattern COLLECTION_PROPERTY = Pattern.compile("(\\S+)?\\[(\\d+)\\](\\.\\S+)?");
/**
* Name of the decrypted property source.
*/
public static final String DECRYPTED_PROPERTY_SOURCE_NAME = "decrypted";
/**
* Prefix indicating an encrypted value.
*/
public static final String ENCRYPTED_PROPERTY_PREFIX = "{cipher}";
protected Log logger = LogFactory.getLog(getClass());
private boolean failOnError = true;
/**
* Strategy to determine how to handle exceptions during decryption.
* @param failOnError the flag value (default true)
*/
public void setFailOnError(boolean failOnError) {
this.failOnError = failOnError;
}
public boolean isFailOnError() {
return this.failOnError;
}
protected Map<String, Object> decrypt(TextEncryptor encryptor, PropertySources propertySources) {
Map<String, Object> properties = merge(propertySources);
decrypt(encryptor, properties);
return properties;
}
protected Map<String, Object> merge(PropertySources propertySources) {
Map<String, Object> properties = new LinkedHashMap<>();
List<PropertySource<?>> sources = new ArrayList<>();
for (PropertySource<?> source : propertySources) {
sources.add(0, source);
}
for (PropertySource<?> source : sources) {
merge(source, properties);
}
return properties;
}
protected void merge(PropertySource<?> source, Map<String, Object> properties) {
if (source instanceof CompositePropertySource) {
List<PropertySource<?>> sources = new ArrayList<>(((CompositePropertySource) source).getPropertySources());
Collections.reverse(sources);
for (PropertySource<?> nested : sources) {
merge(nested, properties);
}
}
else if (source instanceof EnumerablePropertySource) {
Map<String, Object> otherCollectionProperties = new LinkedHashMap<>();
boolean sourceHasDecryptedCollection = false;
EnumerablePropertySource<?> enumerable = (EnumerablePropertySource<?>) source;
for (String key : enumerable.getPropertyNames()) {
Object property = source.getProperty(key);
if (property != null) {
String value = property.toString();
if (value.startsWith(ENCRYPTED_PROPERTY_PREFIX)) {
properties.put(key, value);
if (COLLECTION_PROPERTY.matcher(key).matches()) {
sourceHasDecryptedCollection = true;
}
}
else if (COLLECTION_PROPERTY.matcher(key).matches()) {
// put non-encrypted properties so merging of index properties
// happens correctly
otherCollectionProperties.put(key, value);
}
else {
// override previously encrypted with non-encrypted property
properties.remove(key);
}
}
}
// copy all indexed properties even if not encrypted
if (sourceHasDecryptedCollection && !otherCollectionProperties.isEmpty()) {
properties.putAll(otherCollectionProperties);
}
}
}
protected void decrypt(TextEncryptor encryptor, Map<String, Object> properties) {
properties.replaceAll((key, value) -> {
String valueString = value.toString();
if (!valueString.startsWith(ENCRYPTED_PROPERTY_PREFIX)) {
return value;
}
return decrypt(encryptor, key, valueString);
});
}
protected String decrypt(TextEncryptor encryptor, String key, String original) {
String value = original.substring(ENCRYPTED_PROPERTY_PREFIX.length());
try {
value = encryptor.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.failOnError) {
throw new IllegalStateException(message, e);
}
return "";
}
}
}

View File

@@ -0,0 +1,96 @@
/*
* Copyright 2013-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.encrypt;
import java.util.Map;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.context.properties.bind.Binder;
import org.springframework.boot.env.EnvironmentPostProcessor;
import org.springframework.cloud.bootstrap.TextEncryptorConfigBootstrapper;
import org.springframework.cloud.bootstrap.TextEncryptorConfigBootstrapper.FailsafeTextEncryptor;
import org.springframework.cloud.context.encrypt.EncryptorFactory;
import org.springframework.core.Ordered;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.env.MutablePropertySources;
import org.springframework.core.env.SystemEnvironmentPropertySource;
import org.springframework.security.crypto.encrypt.TextEncryptor;
import org.springframework.util.ClassUtils;
import static org.springframework.cloud.bootstrap.TextEncryptorConfigBootstrapper.keysConfigured;
import static org.springframework.cloud.util.PropertyUtils.bootstrapEnabled;
import static org.springframework.cloud.util.PropertyUtils.useLegacyProcessing;
/**
* Decrypt properties from the environment and insert them with high priority so they
* override the encrypted values.
*
* @author Dave Syer
* @author Tim Ysewyn
*/
public class DecryptEnvironmentPostProcessor extends AbstractEnvironmentDecrypt
implements EnvironmentPostProcessor, Ordered {
private int order = Ordered.LOWEST_PRECEDENCE;
@Override
public int getOrder() {
return this.order;
}
public void setOrder(int order) {
this.order = order;
}
@Override
public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) {
if (bootstrapEnabled(environment) || useLegacyProcessing(environment)) {
return;
}
MutablePropertySources propertySources = environment.getPropertySources();
environment.getPropertySources().remove(DECRYPTED_PROPERTY_SOURCE_NAME);
TextEncryptor encryptor = getTextEncryptor(environment);
Map<String, Object> map = decrypt(encryptor, propertySources);
if (!map.isEmpty()) {
// We have some decrypted properties
propertySources.addFirst(new SystemEnvironmentPropertySource(DECRYPTED_PROPERTY_SOURCE_NAME, map));
}
}
protected TextEncryptor getTextEncryptor(ConfigurableEnvironment environment) {
Binder binder = Binder.get(environment);
KeyProperties keyProperties = binder.bind(KeyProperties.PREFIX, KeyProperties.class)
.orElseGet(KeyProperties::new);
if (keysConfigured(keyProperties)) {
if (ClassUtils.isPresent("org.springframework.security.rsa.crypto.RsaSecretEncryptor", null)) {
RsaProperties rsaProperties = binder.bind(RsaProperties.PREFIX, RsaProperties.class)
.orElseGet(RsaProperties::new);
return TextEncryptorConfigBootstrapper.rsaTextEncryptor(keyProperties, rsaProperties);
}
return new EncryptorFactory(keyProperties.getSalt()).create(keyProperties.getKey());
}
// no keys configured
return new FailsafeTextEncryptor();
}
}

View File

@@ -16,6 +16,7 @@
package org.springframework.cloud.bootstrap.encrypt;
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionOutcome;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
@@ -25,6 +26,7 @@ import org.springframework.boot.autoconfigure.condition.SpringBootCondition;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.cloud.bootstrap.encrypt.KeyProperties.KeyStore;
import org.springframework.cloud.context.encrypt.EncryptorFactory;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ConditionContext;
import org.springframework.context.annotation.Conditional;
@@ -42,55 +44,58 @@ import org.springframework.util.StringUtils;
*/
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass({ TextEncryptor.class })
@EnableConfigurationProperties({ KeyProperties.class })
@EnableConfigurationProperties
public class EncryptionBootstrapConfiguration {
@Autowired(required = false)
private TextEncryptor encryptor;
@Autowired
private KeyProperties key;
@Bean
@ConditionalOnMissingBean
public KeyProperties keyProperties() {
return new KeyProperties();
}
@Bean
public EnvironmentDecryptApplicationInitializer environmentDecryptApplicationListener() {
if (this.encryptor == null) {
this.encryptor = new FailsafeTextEncryptor();
public EnvironmentDecryptApplicationInitializer environmentDecryptApplicationListener(
ConfigurableApplicationContext context, KeyProperties keyProperties) {
TextEncryptor encryptor;
try {
encryptor = context.getBean(TextEncryptor.class);
}
EnvironmentDecryptApplicationInitializer listener = new EnvironmentDecryptApplicationInitializer(
this.encryptor);
listener.setFailOnError(this.key.isFailOnError());
catch (NoSuchBeanDefinitionException e) {
encryptor = new FailsafeTextEncryptor();
}
EnvironmentDecryptApplicationInitializer listener = new EnvironmentDecryptApplicationInitializer(encryptor);
listener.setFailOnError(keyProperties.isFailOnError());
return listener;
}
@Configuration(proxyBeanMethods = false)
@Conditional(KeyCondition.class)
@ConditionalOnClass(RsaSecretEncryptor.class)
@EnableConfigurationProperties({ RsaProperties.class })
@EnableConfigurationProperties
protected static class RsaEncryptionConfiguration {
@Autowired
private KeyProperties key;
@Autowired
private RsaProperties rsaProperties;
@Bean
@ConditionalOnMissingBean
public RsaProperties rsaProperties() {
return new RsaProperties();
}
@Bean
@ConditionalOnMissingBean(TextEncryptor.class)
public TextEncryptor textEncryptor() {
KeyStore keyStore = this.key.getKeyStore();
public TextEncryptor textEncryptor(RsaProperties rsaProperties, KeyProperties keyProperties) {
KeyStore keyStore = keyProperties.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());
rsaProperties.getAlgorithm(), rsaProperties.getSalt(), rsaProperties.isStrong());
}
throw new IllegalStateException("Invalid keystore location");
}
return new EncryptorFactory(this.key.getSalt()).create(this.key.getKey());
return new EncryptorFactory(keyProperties.getSalt()).create(keyProperties.getKey());
}
}

View File

@@ -16,17 +16,10 @@
package org.springframework.cloud.bootstrap.encrypt;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.regex.Pattern;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.cloud.bootstrap.BootstrapApplicationListener;
import org.springframework.cloud.context.environment.EnvironmentChangeEvent;
@@ -34,15 +27,15 @@ import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextInitializer;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.core.Ordered;
import org.springframework.core.env.CompositePropertySource;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.env.EnumerablePropertySource;
import org.springframework.core.env.MutablePropertySources;
import org.springframework.core.env.PropertySource;
import org.springframework.core.env.PropertySources;
import org.springframework.core.env.SystemEnvironmentPropertySource;
import org.springframework.security.crypto.encrypt.TextEncryptor;
import static org.springframework.cloud.util.PropertyUtils.bootstrapEnabled;
import static org.springframework.cloud.util.PropertyUtils.useLegacyProcessing;
/**
* Decrypt properties from the environment and insert them with high priority so they
* override the encrypted values.
@@ -50,42 +43,18 @@ import org.springframework.security.crypto.encrypt.TextEncryptor;
* @author Dave Syer
* @author Tim Ysewyn
*/
public class EnvironmentDecryptApplicationInitializer
public class EnvironmentDecryptApplicationInitializer extends AbstractEnvironmentDecrypt
implements ApplicationContextInitializer<ConfigurableApplicationContext>, Ordered {
/**
* Name of the decrypted property source.
*/
public static final String DECRYPTED_PROPERTY_SOURCE_NAME = "decrypted";
/**
* Name of the decrypted bootstrap property source.
*/
public static final String DECRYPTED_BOOTSTRAP_PROPERTY_SOURCE_NAME = "decryptedBootstrap";
/**
* Prefix indicating an encrypted value.
*/
public static final String ENCRYPTED_PROPERTY_PREFIX = "{cipher}";
private static final Pattern COLLECTION_PROPERTY = Pattern.compile("(\\S+)?\\[(\\d+)\\](\\.\\S+)?");
private static Log logger = LogFactory.getLog(EnvironmentDecryptApplicationInitializer.class);
private int order = Ordered.HIGHEST_PRECEDENCE + 15;
private TextEncryptor encryptor;
private boolean failOnError = true;
/**
* Strategy to determine how to handle exceptions during decryption.
* @param failOnError the flag value (default true)
*/
public void setFailOnError(boolean failOnError) {
this.failOnError = failOnError;
}
public EnvironmentDecryptApplicationInitializer(TextEncryptor encryptor) {
this.encryptor = encryptor;
}
@@ -102,6 +71,10 @@ public class EnvironmentDecryptApplicationInitializer
@Override
public void initialize(ConfigurableApplicationContext applicationContext) {
ConfigurableEnvironment environment = applicationContext.getEnvironment();
if (!bootstrapEnabled(environment) && !useLegacyProcessing(environment)) {
return;
}
MutablePropertySources propertySources = environment.getPropertySources();
Set<String> found = new LinkedHashSet<>();
@@ -119,7 +92,7 @@ public class EnvironmentDecryptApplicationInitializer
}
}
removeDecryptedProperties(applicationContext);
Map<String, Object> map = decrypt(propertySources);
Map<String, Object> map = decrypt(this.encryptor, propertySources);
if (!map.isEmpty()) {
// We have some decrypted properties
found.addAll(map.keySet());
@@ -173,27 +146,9 @@ public class EnvironmentDecryptApplicationInitializer
}
}
public Map<String, Object> decrypt(PropertySources propertySources) {
Map<String, Object> properties = merge(propertySources);
decrypt(properties);
return properties;
}
private Map<String, Object> decrypt(PropertySource<?> source) {
Map<String, Object> properties = merge(source);
decrypt(properties);
return properties;
}
private Map<String, Object> merge(PropertySources propertySources) {
Map<String, Object> properties = new LinkedHashMap<>();
List<PropertySource<?>> sources = new ArrayList<>();
for (PropertySource<?> source : propertySources) {
sources.add(0, source);
}
for (PropertySource<?> source : sources) {
merge(source, properties);
}
decrypt(this.encryptor, properties);
return properties;
}
@@ -203,83 +158,4 @@ public class EnvironmentDecryptApplicationInitializer
return properties;
}
private void merge(PropertySource<?> source, Map<String, Object> properties) {
if (source instanceof CompositePropertySource) {
List<PropertySource<?>> sources = new ArrayList<>(((CompositePropertySource) source).getPropertySources());
Collections.reverse(sources);
for (PropertySource<?> nested : sources) {
merge(nested, properties);
}
}
else if (source instanceof EnumerablePropertySource) {
Map<String, Object> otherCollectionProperties = new LinkedHashMap<>();
boolean sourceHasDecryptedCollection = false;
EnumerablePropertySource<?> enumerable = (EnumerablePropertySource<?>) source;
for (String key : enumerable.getPropertyNames()) {
Object property = source.getProperty(key);
if (property != null) {
String value = property.toString();
if (value.startsWith(ENCRYPTED_PROPERTY_PREFIX)) {
properties.put(key, value);
if (COLLECTION_PROPERTY.matcher(key).matches()) {
sourceHasDecryptedCollection = true;
}
}
else if (COLLECTION_PROPERTY.matcher(key).matches()) {
// put non-encrypted properties so merging of index properties
// happens correctly
otherCollectionProperties.put(key, value);
}
else {
// override previously encrypted with non-encrypted property
properties.remove(key);
}
}
}
// copy all indexed properties even if not encrypted
if (sourceHasDecryptedCollection && !otherCollectionProperties.isEmpty()) {
properties.putAll(otherCollectionProperties);
}
}
}
private void decrypt(Map<String, Object> properties) {
properties.replaceAll((key, value) -> {
String valueString = value.toString();
if (!valueString.startsWith(ENCRYPTED_PROPERTY_PREFIX)) {
return value;
}
return decrypt(key, valueString);
});
}
private String decrypt(String key, String original) {
String value = original.substring(ENCRYPTED_PROPERTY_PREFIX.length());
try {
value = this.encryptor.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.failOnError) {
throw new IllegalStateException(message, e);
}
return "";
}
}
}

View File

@@ -24,9 +24,14 @@ import org.springframework.core.io.Resource;
*
* @author Dave Syer
*/
@ConfigurationProperties("encrypt")
@ConfigurationProperties(KeyProperties.PREFIX)
public class KeyProperties {
/**
* ConfigurationProperties prefix for KeyProperties.
*/
public static final String PREFIX = "encrypt";
/**
* A symmetric key. As a stronger alternative, consider using a keystore.
*/

View File

@@ -24,9 +24,14 @@ import org.springframework.security.rsa.crypto.RsaAlgorithm;
* @author Ryan Baxter
*/
@ConditionalOnClass(RsaAlgorithm.class)
@ConfigurationProperties("encrypt.rsa")
@ConfigurationProperties(RsaProperties.PREFIX)
public class RsaProperties {
/**
* ConfigurationProperties prefix for RsaProperties.
*/
public static final String PREFIX = "encrypt.rsa";
/**
* The RSA algorithm to use (DEFAULT or OEAP). Once it is set, do not change it (or
* existing ciphers will not be decryptable).

View File

@@ -10,10 +10,16 @@ org.springframework.context.ApplicationListener=\
org.springframework.cloud.bootstrap.BootstrapApplicationListener,\
org.springframework.cloud.bootstrap.LoggingSystemShutdownListener,\
org.springframework.cloud.context.restart.RestartListener
# Bootstrap components
# Spring Cloud Bootstrap components
org.springframework.cloud.bootstrap.BootstrapConfiguration=\
org.springframework.cloud.bootstrap.config.PropertySourceBootstrapConfiguration,\
org.springframework.cloud.bootstrap.encrypt.EncryptionBootstrapConfiguration,\
org.springframework.cloud.autoconfigure.ConfigurationPropertiesRebinderAutoConfiguration,\
org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration,\
org.springframework.cloud.util.random.CachedRandomPropertySourceAutoConfiguration
# Spring Boot Bootstrappers
org.springframework.boot.Bootstrapper=\
org.springframework.cloud.bootstrap.TextEncryptorConfigBootstrapper
# Environment Post Processors
org.springframework.boot.env.EnvironmentPostProcessor=\
org.springframework.cloud.bootstrap.encrypt.DecryptEnvironmentPostProcessor

View File

@@ -87,7 +87,7 @@ public class EncryptionBootstrapConfigurationTests {
then(false).as("Should not create an application context with invalid keystore location").isTrue();
}
catch (Exception e) {
then(e).hasRootCauseInstanceOf(IllegalStateException.class);
then(e).isInstanceOf(IllegalStateException.class);
}
}

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 {

View File

@@ -64,7 +64,7 @@ public class EnvironmentDecryptApplicationInitializerTests {
@Test
public void decryptCipherKey() {
ConfigurableApplicationContext context = new AnnotationConfigApplicationContext();
TestPropertyValues.of("foo: {cipher}bar").applyTo(context);
TestPropertyValues.of("spring.cloud.bootstrap.enabled=true", "foo: {cipher}bar").applyTo(context);
this.listener.initialize(context);
then(context.getEnvironment().getProperty("foo")).isEqualTo("bar");
}
@@ -72,8 +72,8 @@ public class EnvironmentDecryptApplicationInitializerTests {
@Test
public void relaxedBinding() {
ConfigurableApplicationContext context = new AnnotationConfigApplicationContext();
TestPropertyValues.of("FOO_TEXT: {cipher}bar").applyTo(context.getEnvironment(),
TestPropertyValues.Type.SYSTEM_ENVIRONMENT);
TestPropertyValues.of("spring.cloud.bootstrap.enabled=true", "FOO_TEXT: {cipher}bar")
.applyTo(context.getEnvironment(), TestPropertyValues.Type.SYSTEM_ENVIRONMENT);
this.listener.initialize(context);
then(context.getEnvironment().getProperty("foo.text")).isEqualTo("bar");
}
@@ -81,7 +81,7 @@ public class EnvironmentDecryptApplicationInitializerTests {
@Test
public void propertySourcesOrderedCorrectly() {
ConfigurableApplicationContext context = new AnnotationConfigApplicationContext();
TestPropertyValues.of("foo: {cipher}bar").applyTo(context);
TestPropertyValues.of("spring.cloud.bootstrap.enabled=true", "foo: {cipher}bar").applyTo(context);
context.getEnvironment().getPropertySources()
.addFirst(new MapPropertySource("test_override", Collections.singletonMap("foo", "{cipher}spam")));
this.listener.initialize(context);
@@ -92,7 +92,7 @@ public class EnvironmentDecryptApplicationInitializerTests {
public void errorOnDecrypt() {
this.listener = new EnvironmentDecryptApplicationInitializer(Encryptors.text("deadbeef", "AFFE37"));
ConfigurableApplicationContext context = new AnnotationConfigApplicationContext();
TestPropertyValues.of("foo: {cipher}bar").applyTo(context);
TestPropertyValues.of("spring.cloud.bootstrap.enabled=true", "foo: {cipher}bar").applyTo(context);
// catch IllegalStateException and verify
try {
this.listener.initialize(context);
@@ -110,7 +110,7 @@ public class EnvironmentDecryptApplicationInitializerTests {
this.listener = new EnvironmentDecryptApplicationInitializer(Encryptors.text("deadbeef", "AFFE37"));
this.listener.setFailOnError(false);
ConfigurableApplicationContext context = new AnnotationConfigApplicationContext();
TestPropertyValues.of("foo: {cipher}bar").applyTo(context);
TestPropertyValues.of("spring.cloud.bootstrap.enabled=true", "foo: {cipher}bar").applyTo(context);
this.listener.initialize(context);
// Assert logs contain warning
String sysOutput = this.outputCapture.toString();
@@ -125,7 +125,9 @@ public class EnvironmentDecryptApplicationInitializerTests {
ConfigurableApplicationContext context = new AnnotationConfigApplicationContext();
// tests that collections in another property source don't get copied into
// "decrypted" property source
TestPropertyValues.of("yours[0].someValue: yourFoo", "yours[1].someValue: yourBar").applyTo(context);
TestPropertyValues
.of("spring.cloud.bootstrap.enabled=true", "yours[0].someValue: yourFoo", "yours[1].someValue: yourBar")
.applyTo(context);
// collection with some encrypted keys and some not encrypted
TestPropertyValues
@@ -153,7 +155,7 @@ public class EnvironmentDecryptApplicationInitializerTests {
EnvironmentDecryptApplicationInitializer initializer = new EnvironmentDecryptApplicationInitializer(
Encryptors.noOpText());
TestPropertyValues.of("key:{cipher}value").applyTo(ctx);
TestPropertyValues.of("spring.cloud.bootstrap.enabled=true", "key:{cipher}value").applyTo(ctx);
ApplicationContext ctxParent = mock(ApplicationContext.class);
when(ctxParent.getEnvironment()).thenReturn(mock(Environment.class));
@@ -168,6 +170,7 @@ public class EnvironmentDecryptApplicationInitializerTests {
@Test
public void testDecryptCompositePropertySource() {
ConfigurableApplicationContext ctx = new AnnotationConfigApplicationContext();
TestPropertyValues.of("spring.cloud.bootstrap.enabled=true").applyTo(ctx);
EnvironmentDecryptApplicationInitializer initializer = new EnvironmentDecryptApplicationInitializer(
Encryptors.noOpText());
@@ -190,7 +193,7 @@ public class EnvironmentDecryptApplicationInitializerTests {
@Test
public void propertySourcesOrderedCorrectlyWithUnencryptedOverrides() {
ConfigurableApplicationContext context = new AnnotationConfigApplicationContext();
TestPropertyValues.of("foo: {cipher}bar").applyTo(context);
TestPropertyValues.of("spring.cloud.bootstrap.enabled=true", "foo: {cipher}bar").applyTo(context);
context.getEnvironment().getPropertySources()
.addFirst(new MapPropertySource("test_override", Collections.singletonMap("foo", "spam")));
this.listener.initialize(context);
@@ -208,6 +211,7 @@ public class EnvironmentDecryptApplicationInitializerTests {
EnvironmentDecryptApplicationInitializer initializer = new EnvironmentDecryptApplicationInitializer(encryptor);
ConfigurableApplicationContext context = new AnnotationConfigApplicationContext();
TestPropertyValues.of("spring.cloud.bootstrap.enabled=true").applyTo(context);
CompositePropertySource bootstrap = new CompositePropertySource(BOOTSTRAP_PROPERTY_SOURCE_NAME);
bootstrap.addPropertySource(
new MapPropertySource("configService", Collections.singletonMap("foo", "{cipher}bar")));
@@ -248,7 +252,8 @@ public class EnvironmentDecryptApplicationInitializerTests {
TextEncryptor encryptor = mock(TextEncryptor.class);
when(encryptor.decrypt("bar2")).thenReturn("bar2");
EnvironmentDecryptApplicationInitializer initializer = new EnvironmentDecryptApplicationInitializer(encryptor);
TestPropertyValues.of("foo: {cipher}bar", "foo2: {cipher}bar2").applyTo(context);
TestPropertyValues.of("spring.cloud.bootstrap.enabled=true", "foo: {cipher}bar", "foo2: {cipher}bar2")
.applyTo(context);
context.getEnvironment().getPropertySources()
.addFirst(new MapPropertySource("test_override", Collections.singletonMap("foo", "spam")));
initializer.initialize(context);