Introduce TextEncryptorLocator abstraction
Allows more customization of encryption and decryption stack. Should be no change for existing users. Fixes gh-124
This commit is contained in:
@@ -1,15 +1,30 @@
|
||||
/*
|
||||
* Copyright 2002-2015 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.cloud.config.client;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import org.springframework.beans.factory.BeanFactoryUtils;
|
||||
import org.springframework.boot.builder.SpringApplicationBuilder;
|
||||
import org.springframework.cloud.config.client.ConfigClientAutoConfiguration;
|
||||
import org.springframework.cloud.config.client.ConfigClientProperties;
|
||||
import org.springframework.context.ConfigurableApplicationContext;
|
||||
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
|
||||
public class ConfigClientAutoConfigurationTests {
|
||||
|
||||
@Test
|
||||
|
||||
@@ -17,9 +17,9 @@ package org.springframework.cloud.config.server;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
|
||||
import org.springframework.cloud.config.server.encryption.EnvironmentEncryptor;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.security.crypto.encrypt.TextEncryptor;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
@@ -29,19 +29,18 @@ import org.springframework.util.StringUtils;
|
||||
@Configuration
|
||||
@ConditionalOnWebApplication
|
||||
public class ConfigServerMvcConfiguration {
|
||||
|
||||
@Autowired(required = false)
|
||||
private TextEncryptor encryptor;
|
||||
|
||||
@Autowired
|
||||
private EnvironmentRepository repository;
|
||||
|
||||
@Autowired
|
||||
private ConfigServerProperties server;
|
||||
|
||||
@Autowired
|
||||
private EnvironmentEncryptor environmentEncryptor;
|
||||
|
||||
@Bean
|
||||
public EnvironmentController environmentController() {
|
||||
EnvironmentController controller = new EnvironmentController(repository, encryptionController());
|
||||
EnvironmentController controller = new EnvironmentController(repository, environmentEncryptor);
|
||||
controller.setDefaultLabel(getDefaultLabel());
|
||||
controller.setOverrides(server.getOverrides());
|
||||
controller.setStripDocumentFromYaml(server.isStripDocumentFromYaml());
|
||||
@@ -56,13 +55,4 @@ public class ConfigServerMvcConfiguration {
|
||||
return repository.getDefaultLabel();
|
||||
}
|
||||
}
|
||||
|
||||
@Bean
|
||||
public EncryptionController encryptionController() {
|
||||
EncryptionController controller = new EncryptionController();
|
||||
if (encryptor!=null) {
|
||||
controller.setEncryptor(encryptor);
|
||||
}
|
||||
return controller;
|
||||
}
|
||||
}
|
||||
@@ -57,6 +57,17 @@ public class ConfigServerProperties {
|
||||
*/
|
||||
private boolean stripDocumentFromYaml = true;
|
||||
|
||||
/**
|
||||
* Default application name when incoming requests do not have a specific one.
|
||||
*/
|
||||
private String defaultApplicationName = "application";
|
||||
|
||||
/**
|
||||
* Default application profile when incoming requests do not have a specific one.
|
||||
*/
|
||||
private String defaultProfile = "default";
|
||||
|
||||
|
||||
public String getDefaultLabel() {
|
||||
return defaultLabel;
|
||||
}
|
||||
@@ -97,4 +108,19 @@ public class ConfigServerProperties {
|
||||
this.stripDocumentFromYaml = stripDocumentFromYaml;
|
||||
}
|
||||
|
||||
public String getDefaultApplicationName() {
|
||||
return defaultApplicationName;
|
||||
}
|
||||
|
||||
public void setDefaultApplicationName(String defaultApplicationName) {
|
||||
this.defaultApplicationName = defaultApplicationName;
|
||||
}
|
||||
|
||||
public String getDefaultProfile() {
|
||||
return defaultProfile;
|
||||
}
|
||||
|
||||
public void setDefaultProfile(String defaultProfile) {
|
||||
this.defaultProfile = defaultProfile;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
import org.springframework.cloud.config.server.encryption.ConfigServerEncryptionConfiguration;
|
||||
import org.springframework.context.annotation.Import;
|
||||
|
||||
/**
|
||||
@@ -30,7 +31,9 @@ import org.springframework.context.annotation.Import;
|
||||
@Target(ElementType.TYPE)
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Documented
|
||||
@Import({ConfigServerConfiguration.class, ConfigServerMvcConfiguration.class})
|
||||
@Import({ConfigServerConfiguration.class,
|
||||
ConfigServerMvcConfiguration.class,
|
||||
ConfigServerEncryptionConfiguration.class})
|
||||
public @interface EnableConfigServer {
|
||||
|
||||
}
|
||||
|
||||
@@ -21,16 +21,13 @@ import java.net.URLDecoder;
|
||||
import java.security.KeyPair;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.Map;
|
||||
import java.util.Map.Entry;
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.cloud.config.environment.Environment;
|
||||
import org.springframework.cloud.config.environment.PropertySource;
|
||||
|
||||
import org.springframework.cloud.config.server.encryption.SingleTextEncryptorLocator;
|
||||
import org.springframework.cloud.config.server.encryption.TextEncryptorLocator;
|
||||
import org.springframework.cloud.context.encrypt.EncryptorFactory;
|
||||
import org.springframework.cloud.context.encrypt.KeyFormatException;
|
||||
import org.springframework.core.io.ByteArrayResource;
|
||||
@@ -44,6 +41,7 @@ import org.springframework.security.rsa.crypto.KeyStoreKeyFactory;
|
||||
import org.springframework.security.rsa.crypto.RsaKeyHolder;
|
||||
import org.springframework.security.rsa.crypto.RsaSecretEncryptor;
|
||||
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestHeader;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
@@ -63,15 +61,19 @@ public class EncryptionController {
|
||||
|
||||
private static Log logger = LogFactory.getLog(EncryptionController.class);
|
||||
|
||||
private TextEncryptor encryptor;
|
||||
private final TextEncryptorLocator encryptorLocator;
|
||||
|
||||
@Autowired(required = false)
|
||||
public void setEncryptor(TextEncryptor encryptor) {
|
||||
this.encryptor = encryptor;
|
||||
private final ConfigServerProperties properties;
|
||||
|
||||
public EncryptionController(TextEncryptorLocator encryptorLocator,
|
||||
ConfigServerProperties configServerProperties) {
|
||||
this.encryptorLocator = encryptorLocator;
|
||||
this.properties = configServerProperties;
|
||||
}
|
||||
|
||||
@RequestMapping(value = "/key", method = RequestMethod.GET)
|
||||
public String getPublicKey() {
|
||||
TextEncryptor encryptor = locateDefaultTextEncryptor();
|
||||
if (!(encryptor instanceof RsaKeyHolder)) {
|
||||
throw new KeyNotAvailableException();
|
||||
}
|
||||
@@ -90,8 +92,9 @@ public class EncryptionController {
|
||||
ByteArrayResource resource = new ByteArrayResource(file.getBytes());
|
||||
KeyPair keyPair = new KeyStoreKeyFactory(resource, password.toCharArray())
|
||||
.getKeyPair(alias);
|
||||
encryptor = new RsaSecretEncryptor(keyPair);
|
||||
body.put("publicKey", ((RsaKeyHolder) encryptor).getPublicKey());
|
||||
RsaSecretEncryptor encryptor = new RsaSecretEncryptor(keyPair);
|
||||
updateEncryptor(encryptor);
|
||||
body.put("publicKey", encryptor.getPublicKey());
|
||||
}
|
||||
catch (IOException e) {
|
||||
throw new KeyFormatException();
|
||||
@@ -102,21 +105,31 @@ public class EncryptionController {
|
||||
|
||||
}
|
||||
|
||||
@RequestMapping(value = "/key", method = RequestMethod.POST, params = { "!password" })
|
||||
@RequestMapping(value = "/key", method = RequestMethod.POST, params = { "!password" })
|
||||
public ResponseEntity<Map<String, Object>> uploadKey(@RequestBody String data,
|
||||
@RequestHeader("Content-Type") MediaType type) {
|
||||
|
||||
Map<String, Object> body = new HashMap<String, Object>();
|
||||
body.put("status", "OK");
|
||||
|
||||
encryptor = new EncryptorFactory().create(stripFormData(data, type, false));
|
||||
|
||||
TextEncryptor encryptor = new EncryptorFactory().create(stripFormData(data, type, false));
|
||||
updateEncryptor(encryptor);
|
||||
if (encryptor instanceof RsaKeyHolder) {
|
||||
body.put("publicKey", ((RsaKeyHolder) encryptor).getPublicKey());
|
||||
}
|
||||
logger.info("Key changed with literal value");
|
||||
return new ResponseEntity<Map<String, Object>>(body, HttpStatus.CREATED);
|
||||
}
|
||||
|
||||
// this is temporary solution to support existing REST API
|
||||
// downcasting is only necessary until we introduce some key management abstraction,
|
||||
// we don't want TextEncryptorLocator to have setEncryptor method
|
||||
private void updateEncryptor(TextEncryptor encryptor) {
|
||||
if (encryptorLocator instanceof SingleTextEncryptorLocator) {
|
||||
((SingleTextEncryptorLocator) encryptorLocator).setEncryptor(encryptor);
|
||||
} else {
|
||||
throw new IncompatibleTextEncryptorLocatorException();
|
||||
}
|
||||
}
|
||||
|
||||
@ExceptionHandler(KeyFormatException.class)
|
||||
@@ -139,39 +152,72 @@ public class EncryptionController {
|
||||
|
||||
@RequestMapping(value = "encrypt/status", method = RequestMethod.GET)
|
||||
public Map<String, Object> status() {
|
||||
if (encryptor == null) {
|
||||
throw new KeyNotInstalledException();
|
||||
}
|
||||
checkEncryptorInstalled(locateDefaultTextEncryptor());
|
||||
return Collections.<String, Object> singletonMap("status", "OK");
|
||||
}
|
||||
|
||||
@RequestMapping(value = "encrypt", method = RequestMethod.POST)
|
||||
public String encrypt(@RequestBody String data,
|
||||
public String encrypt(
|
||||
@RequestBody String data,
|
||||
@RequestHeader("Content-Type") MediaType type) {
|
||||
if (encryptor == null) {
|
||||
throw new KeyNotInstalledException();
|
||||
|
||||
return encrypt(properties.getDefaultApplicationName(),
|
||||
properties.getDefaultProfile(), data, type);
|
||||
}
|
||||
|
||||
@RequestMapping(value = "/encrypt/{name}/{profiles}", method = RequestMethod.POST)
|
||||
public String encrypt(
|
||||
@PathVariable String name,
|
||||
@PathVariable String profiles,
|
||||
@RequestBody String data,
|
||||
@RequestHeader("Content-Type") MediaType type) {
|
||||
|
||||
try {
|
||||
TextEncryptor encryptor = checkEncryptorInstalled(encryptorLocator.locate(name, profiles));
|
||||
String encrypted = encryptor.encrypt(stripFormData(data, type, false));
|
||||
logger.info("Encrypted data");
|
||||
return encrypted;
|
||||
} catch (IllegalArgumentException e) {
|
||||
throw new InvalidCipherException();
|
||||
}
|
||||
data = stripFormData(data, type, false);
|
||||
String encrypted = encryptor.encrypt(data);
|
||||
logger.info("Encrypted data");
|
||||
return encrypted;
|
||||
}
|
||||
|
||||
@RequestMapping(value = "decrypt", method = RequestMethod.POST)
|
||||
public String decrypt(@RequestBody String data,
|
||||
public String decrypt(
|
||||
@RequestBody String data,
|
||||
@RequestHeader("Content-Type") MediaType type) {
|
||||
|
||||
return decrypt(properties.getDefaultApplicationName(),
|
||||
properties.getDefaultProfile(), data, type);
|
||||
}
|
||||
|
||||
@RequestMapping(value = "/decrypt/{name}/{profiles}", method = RequestMethod.POST)
|
||||
public String decrypt(
|
||||
@PathVariable String name,
|
||||
@PathVariable String profiles,
|
||||
@RequestBody String data,
|
||||
@RequestHeader("Content-Type") MediaType type) {
|
||||
|
||||
try {
|
||||
TextEncryptor encryptor = checkEncryptorInstalled(encryptorLocator.locate(name, profiles));
|
||||
String decrypted = encryptor.decrypt(stripFormData(data, type, true));
|
||||
logger.info("Decrypted cipher data");
|
||||
return decrypted;
|
||||
} catch (IllegalArgumentException e) {
|
||||
throw new InvalidCipherException();
|
||||
}
|
||||
}
|
||||
|
||||
private TextEncryptor checkEncryptorInstalled(TextEncryptor encryptor) {
|
||||
if (encryptor == null) {
|
||||
throw new KeyNotInstalledException();
|
||||
}
|
||||
try {
|
||||
data = stripFormData(data, type, true);
|
||||
String decrypted = encryptor.decrypt(data);
|
||||
logger.info("Decrypted cipher data");
|
||||
return decrypted;
|
||||
}
|
||||
catch (IllegalArgumentException e) {
|
||||
throw new InvalidCipherException();
|
||||
}
|
||||
return encryptor;
|
||||
}
|
||||
|
||||
private TextEncryptor locateDefaultTextEncryptor() {
|
||||
return encryptorLocator.locate(properties.getDefaultApplicationName(),
|
||||
properties.getDefaultProfile());
|
||||
}
|
||||
|
||||
private String stripFormData(String data, MediaType type, boolean cipher) {
|
||||
@@ -228,40 +274,6 @@ public class EncryptionController {
|
||||
return new ResponseEntity<Map<String, Object>>(body, HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
|
||||
public Environment decrypt(Environment environment) {
|
||||
Environment result = new Environment(environment.getName(), environment.getProfiles(),
|
||||
environment.getLabel());
|
||||
for (PropertySource source : environment.getPropertySources()) {
|
||||
Map<Object, Object> map = new LinkedHashMap<Object, Object>(
|
||||
source.getSource());
|
||||
for (Entry<Object,Object> entry : new LinkedHashSet<>(map.entrySet())) {
|
||||
Object key = entry.getKey();
|
||||
String name = key.toString();
|
||||
String value = entry.getValue().toString();
|
||||
if (value.startsWith("{cipher}")) {
|
||||
map.remove(key);
|
||||
if (encryptor == null) {
|
||||
map.put(name, value);
|
||||
}
|
||||
else {
|
||||
try {
|
||||
value = value == null ? null : encryptor.decrypt(value
|
||||
.substring("{cipher}".length()));
|
||||
}
|
||||
catch (Exception e) {
|
||||
value = "<n/a>";
|
||||
name = "invalid." + name;
|
||||
logger.warn("Cannot decrypt key: " + key + " ("
|
||||
+ e.getClass() + ": " + e.getMessage() + ")");
|
||||
}
|
||||
map.put(name, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
result.add(new PropertySource(source.getName(), map));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("serial")
|
||||
@@ -275,3 +287,7 @@ class KeyNotAvailableException extends RuntimeException {
|
||||
@SuppressWarnings("serial")
|
||||
class InvalidCipherException extends RuntimeException {
|
||||
}
|
||||
|
||||
@SuppressWarnings("serial")
|
||||
class IncompatibleTextEncryptorLocatorException extends RuntimeException {
|
||||
}
|
||||
|
||||
@@ -31,6 +31,7 @@ import javax.servlet.http.HttpServletResponse;
|
||||
import org.springframework.boot.bind.PropertiesConfigurationFactory;
|
||||
import org.springframework.cloud.config.environment.Environment;
|
||||
import org.springframework.cloud.config.environment.PropertySource;
|
||||
import org.springframework.cloud.config.server.encryption.EnvironmentEncryptor;
|
||||
import org.springframework.core.env.MapPropertySource;
|
||||
import org.springframework.core.env.MutablePropertySources;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
@@ -51,6 +52,9 @@ import org.yaml.snakeyaml.nodes.Tag;
|
||||
* @author Dave Syer
|
||||
* @author Spencer Gibb
|
||||
* @author Roy Clarkson
|
||||
* @author Bartosz Wojtkiewicz
|
||||
* @author Rafal Zukowski
|
||||
*
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("${spring.cloud.config.server.prefix:}")
|
||||
@@ -60,7 +64,7 @@ public class EnvironmentController {
|
||||
|
||||
private EnvironmentRepository repository;
|
||||
|
||||
private EncryptionController encryption;
|
||||
private EnvironmentEncryptor environmentEncryptor;
|
||||
|
||||
private String defaultLabel;
|
||||
|
||||
@@ -68,12 +72,11 @@ public class EnvironmentController {
|
||||
|
||||
private boolean stripDocument = true;
|
||||
|
||||
public EnvironmentController(EnvironmentRepository repository,
|
||||
EncryptionController encryption) {
|
||||
public EnvironmentController(EnvironmentRepository repository, EnvironmentEncryptor environmentEncryptor) {
|
||||
super();
|
||||
this.repository = repository;
|
||||
this.defaultLabel = repository.getDefaultLabel();
|
||||
this.encryption = encryption;
|
||||
this.environmentEncryptor = environmentEncryptor;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -94,7 +97,7 @@ public class EnvironmentController {
|
||||
@RequestMapping("/{name}/{profiles}/{label:.*}")
|
||||
public Environment labelled(@PathVariable String name, @PathVariable String profiles,
|
||||
@PathVariable String label) {
|
||||
Environment environment = encryption.decrypt(repository.findOne(name, profiles,
|
||||
Environment environment = environmentEncryptor.decrypt(repository.findOne(name, profiles,
|
||||
label));
|
||||
if (!overrides.isEmpty()) {
|
||||
environment.addFirst(new PropertySource("overrides", overrides));
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
/*
|
||||
* Copyright 2002-2015 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.cloud.config.server.encryption;
|
||||
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.Map;
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.cloud.config.environment.Environment;
|
||||
import org.springframework.cloud.config.environment.PropertySource;
|
||||
import org.springframework.security.crypto.encrypt.TextEncryptor;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
* EnvironmentEncryptor that can decrypt property values prefixed with {cipher} marker.
|
||||
*
|
||||
* @author Dave Syer
|
||||
* @author Bartosz Wojtkiewicz
|
||||
* @author Rafal Zukowski
|
||||
*
|
||||
*/
|
||||
@Component
|
||||
public class CipherEnvironmentEncryptor implements EnvironmentEncryptor {
|
||||
|
||||
private static Log logger = LogFactory.getLog(CipherEnvironmentEncryptor.class);
|
||||
|
||||
private final TextEncryptorLocator encryptorLocator;
|
||||
|
||||
@Autowired
|
||||
public CipherEnvironmentEncryptor(TextEncryptorLocator encryptorLocator) {
|
||||
this.encryptorLocator = encryptorLocator;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Environment decrypt(Environment environment) {
|
||||
TextEncryptor encryptor = encryptorLocator.locate(environment.getName(),
|
||||
StringUtils.arrayToCommaDelimitedString(environment.getProfiles()));
|
||||
return encryptor != null ? decrypt(environment, encryptor) : environment;
|
||||
}
|
||||
|
||||
private Environment decrypt(Environment environment, TextEncryptor encryptor) {
|
||||
Environment result = new Environment(environment.getName(),
|
||||
environment.getProfiles(), environment.getLabel());
|
||||
for (PropertySource source : environment.getPropertySources()) {
|
||||
Map<Object, Object> map = new LinkedHashMap<Object, Object>(source.getSource());
|
||||
for (Map.Entry<Object, Object> entry : new LinkedHashSet<>(map.entrySet())) {
|
||||
Object key = entry.getKey();
|
||||
String name = key.toString();
|
||||
String value = entry.getValue().toString();
|
||||
if (value.startsWith("{cipher}")) {
|
||||
map.remove(key);
|
||||
try {
|
||||
value = value == null ? null : encryptor.decrypt(value.substring(
|
||||
"{cipher}".length()));
|
||||
} catch (Exception e) {
|
||||
value = "<n/a>";
|
||||
name = "invalid." + name;
|
||||
logger.warn("Cannot decrypt key: " + key
|
||||
+ " (" + e.getClass() + ": " + e.getMessage() + ")");
|
||||
}
|
||||
map.put(name, value);
|
||||
}
|
||||
}
|
||||
result.add(new PropertySource(source.getName(), map));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
/*
|
||||
* Copyright 2002-2015 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.cloud.config.server.encryption;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
|
||||
import org.springframework.cloud.config.server.ConfigServerProperties;
|
||||
import org.springframework.cloud.config.server.EncryptionController;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.security.crypto.encrypt.TextEncryptor;
|
||||
|
||||
/**
|
||||
* @author Bartosz Wojtkiewicz
|
||||
* @author Rafal Zukowski
|
||||
*
|
||||
*/
|
||||
@Configuration
|
||||
public class ConfigServerEncryptionConfiguration {
|
||||
|
||||
@Autowired(required = false)
|
||||
private TextEncryptor encryptor;
|
||||
|
||||
@Autowired
|
||||
private TextEncryptorLocator locator;
|
||||
|
||||
@Autowired
|
||||
private ConfigServerProperties properties;
|
||||
|
||||
@Bean
|
||||
public EncryptionController encryptionController() {
|
||||
return new EncryptionController(locator, properties);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public EnvironmentEncryptor environmentEncryptor() {
|
||||
return new CipherEnvironmentEncryptor(locator);
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
public TextEncryptorLocator textEncryptorLocator() {
|
||||
return new SingleTextEncryptorLocator(encryptor);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
/*
|
||||
* Copyright 2002-2015 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.cloud.config.server.encryption;
|
||||
|
||||
import org.springframework.cloud.config.environment.Environment;
|
||||
|
||||
/**
|
||||
* Service interface for decrypting properties in Environment object.
|
||||
*
|
||||
* @author Bartosz Wojtkiewicz
|
||||
* @author Rafal Zukowski
|
||||
*
|
||||
*/
|
||||
public interface EnvironmentEncryptor {
|
||||
Environment decrypt(Environment environment);
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
/*
|
||||
* Copyright 2002-2015 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.cloud.config.server.encryption;
|
||||
|
||||
import org.springframework.security.crypto.encrypt.TextEncryptor;
|
||||
|
||||
/**
|
||||
* Basic implementation of TextEncryptorLocator which uses
|
||||
* single TextEncryptor for all applications and profiles.
|
||||
*
|
||||
* @author Bartosz Wojtkiewicz
|
||||
* @author Rafal Zukowski
|
||||
*
|
||||
*/
|
||||
public class SingleTextEncryptorLocator implements TextEncryptorLocator {
|
||||
private TextEncryptor encryptor;
|
||||
|
||||
public SingleTextEncryptorLocator() {
|
||||
}
|
||||
|
||||
public SingleTextEncryptorLocator(TextEncryptor encryptor) {
|
||||
this.encryptor = encryptor;
|
||||
}
|
||||
|
||||
// temporary solution to support EncryptionController REST API
|
||||
public void setEncryptor(TextEncryptor encryptor) {
|
||||
this.encryptor = encryptor;
|
||||
}
|
||||
|
||||
@Override
|
||||
public TextEncryptor locate(String applicationName, String profiles) {
|
||||
return encryptor;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
/*
|
||||
* Copyright 2002-2015 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.cloud.config.server.encryption;
|
||||
|
||||
import org.springframework.security.crypto.encrypt.TextEncryptor;
|
||||
|
||||
/**
|
||||
* Service interface for locating proper TextEncryptor to be used for particular application.
|
||||
* It can be used to provide config server with application and environment specific encryption.
|
||||
*
|
||||
* @author Bartosz Wojtkiewicz
|
||||
* @author Rafal Zukowski
|
||||
*
|
||||
*/
|
||||
public interface TextEncryptorLocator {
|
||||
/**
|
||||
* Returns TextEncryptor to be used for given application and profiles.
|
||||
*
|
||||
* @param applicationName application name
|
||||
* @param profiles comma separated list of profiles
|
||||
*/
|
||||
TextEncryptor locate(String applicationName, String profiles);
|
||||
}
|
||||
@@ -1,14 +1,12 @@
|
||||
package org.springframework.cloud.config.server;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import org.junit.BeforeClass;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.mockito.Mockito;
|
||||
|
||||
import org.springframework.beans.factory.BeanFactoryUtils;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
@@ -26,6 +24,11 @@ import org.springframework.test.context.ActiveProfiles;
|
||||
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
|
||||
import org.springframework.test.context.web.WebAppConfiguration;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.BDDMockito.given;
|
||||
import static org.mockito.Matchers.anyString;
|
||||
|
||||
@RunWith(SpringJUnit4ClassRunner.class)
|
||||
@SpringApplicationConfiguration(classes = TestConfiguration.class)
|
||||
@IntegrationTest("server.port:0")
|
||||
@@ -63,7 +66,9 @@ public class ConfigClientOffIntegrationTests {
|
||||
|
||||
@Bean
|
||||
public EnvironmentRepository environmentRepository() {
|
||||
return Mockito.mock(EnvironmentRepository.class);
|
||||
EnvironmentRepository repository = Mockito.mock(EnvironmentRepository.class);
|
||||
given(repository.findOne(anyString(), anyString(), anyString())).willReturn(new Environment("", ""));
|
||||
return repository;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
package org.springframework.cloud.config.server;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import org.junit.BeforeClass;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.mockito.Mockito;
|
||||
|
||||
import org.springframework.beans.factory.BeanFactoryUtils;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
@@ -26,6 +24,11 @@ import org.springframework.test.context.ActiveProfiles;
|
||||
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
|
||||
import org.springframework.test.context.web.WebAppConfiguration;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.BDDMockito.given;
|
||||
import static org.mockito.Matchers.anyString;
|
||||
|
||||
@RunWith(SpringJUnit4ClassRunner.class)
|
||||
@SpringApplicationConfiguration(classes = TestConfiguration.class)
|
||||
@IntegrationTest({ "server.port:0", "spring.cloud.config.enabled:true" })
|
||||
@@ -63,7 +66,9 @@ public class ConfigClientOnIntegrationTests {
|
||||
|
||||
@Bean
|
||||
public EnvironmentRepository environmentRepository() {
|
||||
return Mockito.mock(EnvironmentRepository.class);
|
||||
EnvironmentRepository repository = Mockito.mock(EnvironmentRepository.class);
|
||||
given(repository.findOne(anyString(), anyString(), anyString())).willReturn(new Environment("", ""));
|
||||
return repository;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -15,29 +15,26 @@
|
||||
*/
|
||||
package org.springframework.cloud.config.server;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertNotEquals;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
import java.util.Collections;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.springframework.cloud.config.environment.Environment;
|
||||
import org.springframework.cloud.config.environment.PropertySource;
|
||||
import org.springframework.cloud.config.server.EncryptionController;
|
||||
import org.springframework.cloud.config.server.InvalidCipherException;
|
||||
import org.springframework.cloud.config.server.KeyNotInstalledException;
|
||||
|
||||
import org.springframework.cloud.config.server.encryption.SingleTextEncryptorLocator;
|
||||
import org.springframework.cloud.context.encrypt.KeyFormatException;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.security.rsa.crypto.RsaSecretEncryptor;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertNotEquals;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
/**
|
||||
* @author Dave Syer
|
||||
*
|
||||
*/
|
||||
public class EncryptionControllerTests {
|
||||
|
||||
private EncryptionController controller = new EncryptionController();
|
||||
private SingleTextEncryptorLocator textEncryptorLocator = new SingleTextEncryptorLocator();
|
||||
private ConfigServerProperties properties = new ConfigServerProperties();
|
||||
private EncryptionController controller = new EncryptionController(textEncryptorLocator, properties);
|
||||
|
||||
@Test(expected = KeyNotInstalledException.class)
|
||||
public void cannotDecryptWithoutKey() {
|
||||
@@ -69,38 +66,27 @@ public class EncryptionControllerTests {
|
||||
|
||||
@Test
|
||||
public void sunnyDayRsaKey() {
|
||||
controller.setEncryptor(new RsaSecretEncryptor());
|
||||
textEncryptorLocator.setEncryptor(new RsaSecretEncryptor());
|
||||
String cipher = controller.encrypt("foo", MediaType.TEXT_PLAIN);
|
||||
assertEquals("foo", controller.decrypt(cipher, MediaType.TEXT_PLAIN));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void publicKey() {
|
||||
controller.setEncryptor(new RsaSecretEncryptor());
|
||||
textEncryptorLocator.setEncryptor(new RsaSecretEncryptor());
|
||||
String key = controller.getPublicKey();
|
||||
assertTrue("Wrong key format: " + key, key.startsWith("ssh-rsa"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void formDataIn() {
|
||||
controller.setEncryptor(new RsaSecretEncryptor());
|
||||
textEncryptorLocator.setEncryptor(new RsaSecretEncryptor());
|
||||
// Add space to input
|
||||
String cipher = controller.encrypt("foo bar=", MediaType.APPLICATION_FORM_URLENCODED);
|
||||
String decrypt = controller.decrypt(cipher + "=", MediaType.APPLICATION_FORM_URLENCODED);
|
||||
assertEquals("Wrong decrypted plaintext: " + decrypt, "foo bar", decrypt);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void decryptEnvironment() {
|
||||
controller.uploadKey("foo", MediaType.TEXT_PLAIN);
|
||||
String cipher = controller.encrypt("foo", MediaType.TEXT_PLAIN);
|
||||
Environment environment = new Environment("foo", "bar");
|
||||
environment.add(new PropertySource("spam", Collections
|
||||
.<Object, Object> singletonMap("my", "{cipher}" + cipher)));
|
||||
Environment result = controller.decrypt(environment);
|
||||
assertEquals("foo", result.getPropertySources().get(0).getSource().get("my"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void randomizedCipher() {
|
||||
controller.uploadKey("foo", MediaType.TEXT_PLAIN);
|
||||
|
||||
@@ -19,7 +19,10 @@ package org.springframework.cloud.config.server;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.mockito.Mockito;
|
||||
|
||||
import org.springframework.cloud.config.environment.Environment;
|
||||
import org.springframework.cloud.config.server.encryption.CipherEnvironmentEncryptor;
|
||||
import org.springframework.cloud.config.server.encryption.SingleTextEncryptorLocator;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
|
||||
import org.springframework.test.web.servlet.result.MockMvcResultMatchers;
|
||||
@@ -38,7 +41,7 @@ public class EnvironmentControllerIntegrationTests {
|
||||
public void init() {
|
||||
Mockito.when(repository.getDefaultLabel()).thenReturn("master");
|
||||
mvc = MockMvcBuilders.standaloneSetup(
|
||||
new EnvironmentController(repository, new EncryptionController()))
|
||||
new EnvironmentController(repository, new CipherEnvironmentEncryptor(new SingleTextEncryptorLocator())))
|
||||
.build();
|
||||
}
|
||||
|
||||
|
||||
@@ -28,6 +28,8 @@ import org.mockito.Mockito;
|
||||
|
||||
import org.springframework.cloud.config.environment.Environment;
|
||||
import org.springframework.cloud.config.environment.PropertySource;
|
||||
import org.springframework.cloud.config.server.encryption.CipherEnvironmentEncryptor;
|
||||
import org.springframework.cloud.config.server.encryption.SingleTextEncryptorLocator;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
|
||||
@@ -55,7 +57,7 @@ public class EnvironmentControllerTests {
|
||||
@Before
|
||||
public void init() {
|
||||
Mockito.when(repository.getDefaultLabel()).thenReturn("master");
|
||||
this.controller = new EnvironmentController(repository, new EncryptionController());
|
||||
this.controller = new EnvironmentController(repository, new CipherEnvironmentEncryptor(new SingleTextEncryptorLocator()));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
/*
|
||||
* Copyright 2002-2015 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.cloud.config.server.encryption;
|
||||
|
||||
import java.util.Collections;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import org.springframework.cloud.config.environment.Environment;
|
||||
import org.springframework.cloud.config.environment.PropertySource;
|
||||
import org.springframework.cloud.context.encrypt.EncryptorFactory;
|
||||
import org.springframework.security.crypto.encrypt.TextEncryptor;
|
||||
|
||||
import static java.util.UUID.randomUUID;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
|
||||
public class CipherEnvironmentEncryptorTests {
|
||||
TextEncryptor textEncryptor = new EncryptorFactory().create("foo");
|
||||
TextEncryptorLocator textEncryptorLocator = new SingleTextEncryptorLocator(textEncryptor);
|
||||
EnvironmentEncryptor encryptor = new CipherEnvironmentEncryptor(textEncryptorLocator);
|
||||
|
||||
@Test
|
||||
public void shouldDecryptEnvironment() {
|
||||
// given
|
||||
String secret = randomUUID().toString();
|
||||
|
||||
// when
|
||||
Environment environment = new Environment("name", "profile", "label");
|
||||
environment.add(new PropertySource("a",
|
||||
Collections.<Object, Object>singletonMap(environment.getName(), "{cipher}" + textEncryptor.encrypt(secret))));
|
||||
|
||||
// then
|
||||
assertEquals(secret, encryptor.decrypt(environment).getPropertySources().get(0).getSource().get(environment.getName()));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user