diff --git a/spring-cloud-context/src/main/java/org/springframework/cloud/bootstrap/TextEncryptorBindHandler.java b/spring-cloud-context/src/main/java/org/springframework/cloud/bootstrap/TextEncryptorBindHandler.java new file mode 100644 index 000000000..5c14c1609 --- /dev/null +++ b/spring-cloud-context/src/main/java/org/springframework/cloud/bootstrap/TextEncryptorBindHandler.java @@ -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 ""; + } + } + +} 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 000000000..c4dca2797 --- /dev/null +++ b/spring-cloud-context/src/main/java/org/springframework/cloud/bootstrap/TextEncryptorConfigBootstrapper.java @@ -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?"); + } + + } + +} diff --git a/spring-cloud-context/src/main/java/org/springframework/cloud/bootstrap/encrypt/AbstractEnvironmentDecrypt.java b/spring-cloud-context/src/main/java/org/springframework/cloud/bootstrap/encrypt/AbstractEnvironmentDecrypt.java new file mode 100644 index 000000000..c2d4781cc --- /dev/null +++ b/spring-cloud-context/src/main/java/org/springframework/cloud/bootstrap/encrypt/AbstractEnvironmentDecrypt.java @@ -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 decrypt(TextEncryptor encryptor, PropertySources propertySources) { + Map properties = merge(propertySources); + decrypt(encryptor, properties); + return properties; + } + + protected Map merge(PropertySources propertySources) { + Map properties = new LinkedHashMap<>(); + List> 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 properties) { + if (source instanceof CompositePropertySource) { + + List> sources = new ArrayList<>(((CompositePropertySource) source).getPropertySources()); + Collections.reverse(sources); + + for (PropertySource nested : sources) { + merge(nested, properties); + } + + } + else if (source instanceof EnumerablePropertySource) { + Map 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 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 ""; + } + } + +} diff --git a/spring-cloud-context/src/main/java/org/springframework/cloud/bootstrap/encrypt/DecryptEnvironmentPostProcessor.java b/spring-cloud-context/src/main/java/org/springframework/cloud/bootstrap/encrypt/DecryptEnvironmentPostProcessor.java new file mode 100644 index 000000000..752273a06 --- /dev/null +++ b/spring-cloud-context/src/main/java/org/springframework/cloud/bootstrap/encrypt/DecryptEnvironmentPostProcessor.java @@ -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 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(); + } + +} diff --git a/spring-cloud-context/src/main/java/org/springframework/cloud/bootstrap/encrypt/EncryptionBootstrapConfiguration.java b/spring-cloud-context/src/main/java/org/springframework/cloud/bootstrap/encrypt/EncryptionBootstrapConfiguration.java index 2619ad865..c77b511f8 100644 --- a/spring-cloud-context/src/main/java/org/springframework/cloud/bootstrap/encrypt/EncryptionBootstrapConfiguration.java +++ b/spring-cloud-context/src/main/java/org/springframework/cloud/bootstrap/encrypt/EncryptionBootstrapConfiguration.java @@ -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.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 @@ */ @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()); } } diff --git a/spring-cloud-context/src/main/java/org/springframework/cloud/bootstrap/encrypt/EnvironmentDecryptApplicationInitializer.java b/spring-cloud-context/src/main/java/org/springframework/cloud/bootstrap/encrypt/EnvironmentDecryptApplicationInitializer.java index 1b0613985..dc7de109c 100644 --- a/spring-cloud-context/src/main/java/org/springframework/cloud/bootstrap/encrypt/EnvironmentDecryptApplicationInitializer.java +++ b/spring-cloud-context/src/main/java/org/springframework/cloud/bootstrap/encrypt/EnvironmentDecryptApplicationInitializer.java @@ -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.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 @@ * @author Dave Syer * @author Tim Ysewyn */ -public class EnvironmentDecryptApplicationInitializer +public class EnvironmentDecryptApplicationInitializer extends AbstractEnvironmentDecrypt implements ApplicationContextInitializer, 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 void setOrder(int order) { @Override public void initialize(ConfigurableApplicationContext applicationContext) { ConfigurableEnvironment environment = applicationContext.getEnvironment(); + if (!bootstrapEnabled(environment) && !useLegacyProcessing(environment)) { + return; + } + MutablePropertySources propertySources = environment.getPropertySources(); Set found = new LinkedHashSet<>(); @@ -119,7 +92,7 @@ public void initialize(ConfigurableApplicationContext applicationContext) { } } removeDecryptedProperties(applicationContext); - Map map = decrypt(propertySources); + Map map = decrypt(this.encryptor, propertySources); if (!map.isEmpty()) { // We have some decrypted properties found.addAll(map.keySet()); @@ -173,27 +146,9 @@ private void removeDecryptedProperties(ApplicationContext applicationContext) { } } - public Map decrypt(PropertySources propertySources) { - Map properties = merge(propertySources); - decrypt(properties); - return properties; - } - private Map decrypt(PropertySource source) { Map properties = merge(source); - decrypt(properties); - return properties; - } - - private Map merge(PropertySources propertySources) { - Map properties = new LinkedHashMap<>(); - List> 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 @@ private Map merge(PropertySource source) { return properties; } - private void merge(PropertySource source, Map properties) { - if (source instanceof CompositePropertySource) { - - List> sources = new ArrayList<>(((CompositePropertySource) source).getPropertySources()); - Collections.reverse(sources); - - for (PropertySource nested : sources) { - merge(nested, properties); - } - - } - else if (source instanceof EnumerablePropertySource) { - Map 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 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 ""; - } - } - } diff --git a/spring-cloud-context/src/main/java/org/springframework/cloud/bootstrap/encrypt/KeyProperties.java b/spring-cloud-context/src/main/java/org/springframework/cloud/bootstrap/encrypt/KeyProperties.java index fcaff5c6c..112f79b83 100644 --- a/spring-cloud-context/src/main/java/org/springframework/cloud/bootstrap/encrypt/KeyProperties.java +++ b/spring-cloud-context/src/main/java/org/springframework/cloud/bootstrap/encrypt/KeyProperties.java @@ -24,9 +24,14 @@ * * @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. */ diff --git a/spring-cloud-context/src/main/java/org/springframework/cloud/bootstrap/encrypt/RsaProperties.java b/spring-cloud-context/src/main/java/org/springframework/cloud/bootstrap/encrypt/RsaProperties.java index 9962ac643..6f9318834 100644 --- a/spring-cloud-context/src/main/java/org/springframework/cloud/bootstrap/encrypt/RsaProperties.java +++ b/spring-cloud-context/src/main/java/org/springframework/cloud/bootstrap/encrypt/RsaProperties.java @@ -24,9 +24,14 @@ * @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). 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 0fde0d1bc..16deea638 100644 --- a/spring-cloud-context/src/main/resources/META-INF/spring.factories +++ b/spring-cloud-context/src/main/resources/META-INF/spring.factories @@ -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 \ No newline at end of file diff --git a/spring-cloud-context/src/test/java/org/springframework/cloud/bootstrap/encrypt/EncryptionBootstrapConfigurationTests.java b/spring-cloud-context/src/test/java/org/springframework/cloud/bootstrap/encrypt/EncryptionBootstrapConfigurationTests.java index 83e2dd7b0..024fad3f1 100644 --- a/spring-cloud-context/src/test/java/org/springframework/cloud/bootstrap/encrypt/EncryptionBootstrapConfigurationTests.java +++ b/spring-cloud-context/src/test/java/org/springframework/cloud/bootstrap/encrypt/EncryptionBootstrapConfigurationTests.java @@ -87,7 +87,7 @@ public void nonExistentKeystoreLocationShouldNotBeAllowed() { 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); } } 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 9def18ecd..55ba62f7c 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 @@ 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 @@ 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 void symmetricPropertyValues() { } @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 void symmetricConfigurationProperties() { 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 { diff --git a/spring-cloud-context/src/test/java/org/springframework/cloud/bootstrap/encrypt/EnvironmentDecryptApplicationInitializerTests.java b/spring-cloud-context/src/test/java/org/springframework/cloud/bootstrap/encrypt/EnvironmentDecryptApplicationInitializerTests.java index eafdb957c..2fb1bdacc 100644 --- a/spring-cloud-context/src/test/java/org/springframework/cloud/bootstrap/encrypt/EnvironmentDecryptApplicationInitializerTests.java +++ b/spring-cloud-context/src/test/java/org/springframework/cloud/bootstrap/encrypt/EnvironmentDecryptApplicationInitializerTests.java @@ -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 void decryptCipherKey() { @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 void relaxedBinding() { @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 void propertySourcesOrderedCorrectly() { 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 void errorOnDecryptWithEmpty() { 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 void indexedPropertiesCopied() { 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 void testDecryptNonStandardParent() { 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 void testDecryptNonStandardParent() { @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 void testDecryptCompositePropertySource() { @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 void doNotDecryptBootstrapTwice() { 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 void testOnlyDecryptIfNotOverridden() { 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);