/* * Copyright 2016 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 * * http://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.xd.dirt.container.decryptor; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.BeansException; import org.springframework.boot.context.event.ApplicationPreparedEvent; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; 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.MapPropertySource; import org.springframework.core.env.MutablePropertySources; import org.springframework.core.env.PropertySource; import org.springframework.core.env.PropertySources; import org.springframework.security.crypto.encrypt.TextEncryptor; import org.springframework.xd.dirt.container.initializer.OrderedContextInitializer; import java.util.ArrayList; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; /** * Decrypt properties from the environment and insert them with high priority so they * override the encrypted values. This class is injected as an ApplicationListener in * each application context in the XD hierarchy to be invoked before the context is * refreshed. This class is loosely based on spring-cloud-commons * org.springframework.cloud.bootstrap.encrypt.EnvironmentDecryptApplicationInitializer, * adopting the same conventions used to identify and process encrypted property values * in Spring Cloud applications. * * @author David Turanski * @since 1.3.1 */ public class PropertiesDecryptor implements OrderedContextInitializer, ApplicationContextAware { private static Logger logger = LoggerFactory.getLogger(PropertiesDecryptor.class); public static final String DECRYPTED_PROPERTY_SOURCE_NAME = "decrypted"; public static final String DECRYPTOR_BEAN_NAME = "propertiesDecryptor"; private int order = Ordered.HIGHEST_PRECEDENCE + 15; private TextEncryptor decryptor; private boolean failOnError = true; private ApplicationContext applicationContext; /** * * @param decryptor the {@link TextEncryptor} used to decrypt properties. * A null is ok here but properties will not be decrypted. */ public PropertiesDecryptor(TextEncryptor decryptor) { this.decryptor = decryptor; } /** * Strategy to determine how to handle exceptions during decryption. * * @param failOnError the flag value (default true) */ public void setFailOnError(boolean failOnError) { this.failOnError = failOnError; } @Override public int getOrder() { return this.order; } public void setOrder(int order) { this.order = order; } @Override public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { this.applicationContext = applicationContext; } @Override public void onApplicationEvent(ApplicationPreparedEvent event) { if (this.decryptor == null) { return; } ConfigurableApplicationContext applicationContext = event .getApplicationContext(); if (!applicationContext.equals(this.applicationContext)){ return; } /* * register the decryptor as a bean so it is available to the * PropertiesDecryptorPlugin later. */ if (!applicationContext.containsBean(DECRYPTOR_BEAN_NAME)) { applicationContext.getBeanFactory().registerSingleton (DECRYPTOR_BEAN_NAME, decryptor); } ConfigurableEnvironment environment = applicationContext.getEnvironment(); MutablePropertySources propertySources = environment.getPropertySources(); Map<String, Object> map = decrypt(propertySources); if (!map.isEmpty()) { insert(propertySources, new MapPropertySource(DECRYPTED_PROPERTY_SOURCE_NAME, map)); } } private void insert(MutablePropertySources propertySources, MapPropertySource propertySource) { propertySources.addFirst(propertySource); } public Map<String, Object> decrypt(PropertySources propertySources) { Map<String, Object> overrides = new LinkedHashMap<String, Object>(); List<PropertySource<?>> sources = new ArrayList<PropertySource<?>>(); for (PropertySource<?> source : propertySources) { sources.add(0, source); } for (PropertySource<?> source : sources) { decrypt(source, overrides); } return overrides; } private Map<String, Object> decrypt(PropertySource<?> source) { Map<String, Object> overrides = new LinkedHashMap<String, Object>(); decrypt(source, overrides); return overrides; } private void decrypt(PropertySource<?> source, Map<String, Object> overrides) { if (source instanceof EnumerablePropertySource) { EnumerablePropertySource<?> enumerable = (EnumerablePropertySource<?>) source; for (String key : enumerable.getPropertyNames()) { Object property = source.getProperty(key); if (property != null) { String value = property.toString(); if (value.startsWith("{cipher}")) { value = value.substring("{cipher}".length()); try { value = this.decryptor.decrypt(value); logger.debug("Decrypted: key=" + key); } catch (Exception e) { String message = "Cannot decrypt: key=" + key; if (this.failOnError) { throw new IllegalStateException(message, e); } if (logger.isDebugEnabled()) { logger.warn(message, e); } else { logger.warn(message); } // Set value to empty to avoid making a password out of the // cipher text value = ""; } overrides.put(key, value); } } } } else if (source instanceof CompositePropertySource) { for (PropertySource<?> nested : ((CompositePropertySource) source) .getPropertySources()) { decrypt(nested, overrides); } } else { logger.debug("ignored property source {} {}", source.getName(), source .getClass().getName()); } } }