From 1504d6cb0c67dc37866e1fa773c35fe60b915e19 Mon Sep 17 00:00:00 2001 From: Dave Syer Date: Tue, 9 Jun 2015 13:13:35 +0100 Subject: [PATCH] Provide options for key rotation and per-app cryptography Cipher text (and plain text in the /encrypt endpoint) can now carry a prefix of {name:value} pairs, all of which are passed into the TextEncryptorLocator (instead of just the app name and profiles). The inputs are prepared symmetrically by the EncryptionController (if used) and the EnvironmentEncryptor (used in the EnvironmentController). Tidy up docs and add notes on keys. --- .../main/asciidoc/spring-cloud-config.adoc | 61 +++++-- .../ConfigServerBootstrapConfiguration.java | 1 + .../config/server/EnableConfigServer.java | 4 +- .../ConfigServerEncryptionConfiguration.java | 16 +- .../ConfigServerMvcConfiguration.java | 5 +- .../config/EncryptionAutoConfiguration.java | 87 ++++++++++ .../EnvironmentRepositoryConfiguration.java | 8 +- .../SingleEncryptorAutoConfiguration.java} | 36 ++--- .../CipherEnvironmentEncryptor.java | 36 +++-- .../EncryptionController.java | 128 +++++---------- .../encryption/EnvironmentPrefixHelper.java | 128 +++++++++++++++ .../KeyStoreTextEncryptorLocator.java | 71 ++++++++ .../encryption/PassthruSecretLocator.java | 30 ++++ .../server/encryption/SecretLocator.java | 27 ++++ .../SingleTextEncryptorLocator.java | 26 ++- .../encryption/TextEncryptorLocator.java | 19 +-- .../main/resources/META-INF/spring.factories | 3 +- ...tionControllerMultiTextEncryptorTests.java | 77 --------- .../server/EncryptionControllerTests.java | 103 ------------ ...EnvironmentControllerIntegrationTests.java | 23 ++- .../server/EnvironmentControllerTests.java | 151 +++++++++--------- ...EnvironmentRepositoryIntegrationTests.java | 1 + ...EnvironmentRepositoryIntegrationTests.java | 2 +- ...EnvironmentRepositoryIntegrationTests.java | 2 +- .../CipherEnvironmentEncryptorTests.java | 56 ++++--- ...tionControllerMultiTextEncryptorTests.java | 62 +++++++ .../encryption/EncryptionControllerTests.java | 124 ++++++++++++++ .../EnvironmentPrefixHelperTests.java | 84 ++++++++++ .../KeyStoreTextEncryptorLocatorTests.java | 72 +++++++++ .../src/test/resources/server.jks | Bin 0 -> 4451 bytes 30 files changed, 975 insertions(+), 468 deletions(-) rename spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/{encryption => config}/ConfigServerEncryptionConfiguration.java (76%) rename spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/{ => config}/ConfigServerMvcConfiguration.java (87%) create mode 100644 spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/config/EncryptionAutoConfiguration.java rename spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/{ => config}/EnvironmentRepositoryConfiguration.java (81%) rename spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/{encryption/EncryptionAutoConfiguration.java => config/SingleEncryptorAutoConfiguration.java} (56%) rename spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/{ => encryption}/EncryptionController.java (61%) create mode 100644 spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/encryption/EnvironmentPrefixHelper.java create mode 100644 spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/encryption/KeyStoreTextEncryptorLocator.java create mode 100644 spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/encryption/PassthruSecretLocator.java create mode 100644 spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/encryption/SecretLocator.java delete mode 100644 spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/EncryptionControllerMultiTextEncryptorTests.java delete mode 100644 spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/EncryptionControllerTests.java create mode 100644 spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/encryption/EncryptionControllerMultiTextEncryptorTests.java create mode 100644 spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/encryption/EncryptionControllerTests.java create mode 100644 spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/encryption/EnvironmentPrefixHelperTests.java create mode 100644 spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/encryption/KeyStoreTextEncryptorLocatorTests.java create mode 100644 spring-cloud-config-server/src/test/resources/server.jks diff --git a/docs/src/main/asciidoc/spring-cloud-config.adoc b/docs/src/main/asciidoc/spring-cloud-config.adoc index f60f7ec6..c30d8e87 100644 --- a/docs/src/main/asciidoc/spring-cloud-config.adoc +++ b/docs/src/main/asciidoc/spring-cloud-config.adoc @@ -261,15 +261,13 @@ You can download the "Java Cryptography Extension (JCE) Unlimited Strength Juris from Oracle, and follow instructions for installation (essentially replace the 2 policy files in the JRE lib/security directory with the ones that you downloaded). -The server exposes `/encrypt` and `/decrypt` endpoints (on the -assumption that these will be secured and only accessed by authorized -agents). If the remote property sources contain encryted content +If the remote property sources contain encryted content (values starting with `{cipher}`) they will be decrypted before sending to clients over HTTP. The main advantage of this set up is that the property values don't have to be in plain text when they are "at rest" (e.g. in a git repository). If a value cannot be decrypted it is replaced with an empty string, largely to prevent cipher text -being used as a password in Spring Boot autconfigured HTTP basic. +being used as a password and accidentally leaking. If you are setting up a remote config repository for config client applications it might contain an `application.yml` like this, for @@ -286,7 +284,9 @@ spring: You can safely push this plain text to a shared git repository and the secret password is protected. -If you are editing a remote config file you can use the Config Server +The server also exposes `/encrypt` and `/decrypt` endpoints (on the +assumption that these will be secured and only accessed by authorized +agents). If you are editing a remote config file you can use the Config Server to encrypt values by POSTing to the `/encrypt` endpoint, e.g. ---- @@ -304,7 +304,15 @@ mysecret Take the encypted value and add the `{cipher}` prefix before you put it in the YAML or properties file, and before you commit and push it -to a remote, potentially insecure store. +to a remote, potentially insecure store. The `/encypt` and `/decrypt` +endpoints also both accept paths of the form `/*/{name}/{profiles}` +which can be used to control cryptography per application (name) +and profile when clients call into the main Environment resource. + +NOTE: to control the cryptography in this granular way you must also +provide a `@Bean` of type `TextEncryptorLocator` that creates a +different encryptor per name and profiles. The one that is provided +by default does not do this. The `spring` command line client (with Spring Cloud CLI extensions installed) can also be used to encrypt and decrypt, e.g. @@ -335,9 +343,7 @@ it is just a single property value to configure. To configure a symmetric key you just need to set `encrypt.key` to a secret String (or use an enviroment variable `ENCRYPT_KEY` to keep it -out of plain text configuration files). You can also POST a key value -to the `/key` endpoint (but that won't change any existing encrypted -values in remote repositories). +out of plain text configuration files). To configure an asymmetric key you can either set the key as a PEM-encoded text value (in `encrypt.key`), or via a keystore (e.g. as @@ -381,6 +387,43 @@ encrypt: secret: changeme ---- +=== Using Multiple Keys and Key Rotation + +In addition to the `{cipher}` prefix in encrypted property values, the +Config Server looks for `{name:value}` prefixes (zero or many) before +the start of the (Base64 encoded) cipher text. The keys are passed to +a `TextEncryptorLocator` which can do whatever logic it needs to +locate a `TextEncryptor` for the cipher. If you have configured a +keystore (`encrypt.keystore.location`) the default locator will look +for keys in the store with aliases as supplied by the "key" prefix, +i.e. with a cipher text like this: + + +---- +foo: + bar: `{cipher}{key:testkey}...` +---- + +the locator will look for a key named "testkey". A secret can also be +supplied via a `{secret:...}` value in the prefix, but if it is not +the default is to use the keystore password (which is what you get +when you build a keytore and don't specify a secret). If you *do* +supply a secret it is recommended that you also encrypt the secrets +using a custom `SecretLocator`. + +Key rotation is hardly ever necessary on cryptographic grounds if the +keys are only being used to encrypt a few bytes of configuration data +(i.e. they are not being used elsewhere), but occasionally you might +need to change the keys if there is a security breach for instance. In +that case all the clients would need to change their source config +files (e.g. in git) and use a new `{key:...}` prefix in all the +ciphers, checking beforehand of course that the key alias is available +in the Config Server keystore. + +TIP: the `{name:value}` prefixes can also be added to plaintext posted +to the `/encrypt` endpoint, if you want to let the Config Server +handle all encryption as well as decryption. + === Embedding the Config Server The Config Server runs best as a standalone application, but if you diff --git a/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/ConfigServerBootstrapConfiguration.java b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/ConfigServerBootstrapConfiguration.java index 621a8b42..4b885c61 100644 --- a/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/ConfigServerBootstrapConfiguration.java +++ b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/ConfigServerBootstrapConfiguration.java @@ -18,6 +18,7 @@ package org.springframework.cloud.config.server; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.cloud.config.client.ConfigClientProperties; +import org.springframework.cloud.config.server.config.EnvironmentRepositoryConfiguration; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; diff --git a/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/EnableConfigServer.java b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/EnableConfigServer.java index 65a4e027..48685392 100644 --- a/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/EnableConfigServer.java +++ b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/EnableConfigServer.java @@ -21,7 +21,9 @@ 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.cloud.config.server.config.ConfigServerEncryptionConfiguration; +import org.springframework.cloud.config.server.config.ConfigServerMvcConfiguration; +import org.springframework.cloud.config.server.config.EnvironmentRepositoryConfiguration; import org.springframework.context.annotation.Import; /** diff --git a/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/encryption/ConfigServerEncryptionConfiguration.java b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/config/ConfigServerEncryptionConfiguration.java similarity index 76% rename from spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/encryption/ConfigServerEncryptionConfiguration.java rename to spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/config/ConfigServerEncryptionConfiguration.java index 44d63aed..671898c3 100644 --- a/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/encryption/ConfigServerEncryptionConfiguration.java +++ b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/config/ConfigServerEncryptionConfiguration.java @@ -14,11 +14,12 @@ * limitations under the License. */ -package org.springframework.cloud.config.server.encryption; +package org.springframework.cloud.config.server.config; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.cloud.config.server.ConfigServerProperties; -import org.springframework.cloud.config.server.EncryptionController; +import org.springframework.cloud.config.server.encryption.EncryptionController; +import org.springframework.cloud.config.server.encryption.TextEncryptorLocator; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -30,20 +31,15 @@ import org.springframework.context.annotation.Configuration; @Configuration public class ConfigServerEncryptionConfiguration { - @Autowired - private TextEncryptorLocator locator; + @Autowired(required=false) + private TextEncryptorLocator encryptor; @Autowired private ConfigServerProperties properties; - @Bean - public EnvironmentEncryptor environmentEncryptor() { - return new CipherEnvironmentEncryptor(locator); - } - @Bean public EncryptionController encryptionController() { - return new EncryptionController(locator, properties); + return new EncryptionController(this.encryptor, this.properties); } } diff --git a/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/ConfigServerMvcConfiguration.java b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/config/ConfigServerMvcConfiguration.java similarity index 87% rename from spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/ConfigServerMvcConfiguration.java rename to spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/config/ConfigServerMvcConfiguration.java index e2521980..5b942bc4 100644 --- a/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/ConfigServerMvcConfiguration.java +++ b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/config/ConfigServerMvcConfiguration.java @@ -13,10 +13,13 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.cloud.config.server; +package org.springframework.cloud.config.server.config; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; +import org.springframework.cloud.config.server.ConfigServerProperties; +import org.springframework.cloud.config.server.EnvironmentController; +import org.springframework.cloud.config.server.EnvironmentRepository; import org.springframework.cloud.config.server.encryption.EnvironmentEncryptor; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; diff --git a/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/config/EncryptionAutoConfiguration.java b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/config/EncryptionAutoConfiguration.java new file mode 100644 index 00000000..a085fce6 --- /dev/null +++ b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/config/EncryptionAutoConfiguration.java @@ -0,0 +1,87 @@ +/* + * 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.config; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.cloud.bootstrap.encrypt.KeyProperties; +import org.springframework.cloud.bootstrap.encrypt.KeyProperties.KeyStore; +import org.springframework.cloud.config.server.encryption.CipherEnvironmentEncryptor; +import org.springframework.cloud.config.server.encryption.EnvironmentEncryptor; +import org.springframework.cloud.config.server.encryption.KeyStoreTextEncryptorLocator; +import org.springframework.cloud.config.server.encryption.TextEncryptorLocator; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.crypto.encrypt.Encryptors; +import org.springframework.security.crypto.encrypt.TextEncryptor; +import org.springframework.security.rsa.crypto.KeyStoreKeyFactory; +import org.springframework.security.rsa.crypto.RsaSecretEncryptor; + +/** + * Auto configuration for text encryptors and environment encryptors (non-web stuff). + * Users can provide beans of the same type as any or all of the beans defined here in + * application code to override the default behaviour. + * + * @author Bartosz Wojtkiewicz + * @author Rafal Zukowski + * @author Dave Syer + * + */ +@Configuration +public class EncryptionAutoConfiguration { + + @ConditionalOnMissingBean(TextEncryptor.class) + protected static class DefaultTextEncryptorConfiguration { + + @Bean + public TextEncryptor nullTextEncryptor() { + return Encryptors.noOpText(); + } + + } + + @Bean + @ConditionalOnMissingBean + public EnvironmentEncryptor environmentEncryptor( + TextEncryptorLocator textEncryptorLocator) { + return new CipherEnvironmentEncryptor(textEncryptorLocator); + } + + @Configuration + @ConditionalOnClass(RsaSecretEncryptor.class) + @ConditionalOnProperty(value = "encrypt.keyStore.location", matchIfMissing = false) + @EnableConfigurationProperties(KeyProperties.class) + protected static class KeyStoreConfiguration { + + @Autowired + private KeyProperties key; + + @Bean + @ConditionalOnMissingBean + public TextEncryptorLocator textEncryptorLocator() { + KeyStore keyStore = this.key.getKeyStore(); + return new KeyStoreTextEncryptorLocator(new KeyStoreKeyFactory( + keyStore.getLocation(), keyStore.getPassword().toCharArray()), + keyStore.getSecret(), keyStore.getAlias()); + } + + } + +} diff --git a/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/EnvironmentRepositoryConfiguration.java b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/config/EnvironmentRepositoryConfiguration.java similarity index 81% rename from spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/EnvironmentRepositoryConfiguration.java rename to spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/config/EnvironmentRepositoryConfiguration.java index 019e8763..13163754 100644 --- a/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/EnvironmentRepositoryConfiguration.java +++ b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/config/EnvironmentRepositoryConfiguration.java @@ -13,12 +13,18 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.cloud.config.server; +package org.springframework.cloud.config.server.config; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.cloud.config.server.ConfigServerHealthIndicator; +import org.springframework.cloud.config.server.ConfigServerProperties; +import org.springframework.cloud.config.server.EnvironmentRepository; +import org.springframework.cloud.config.server.MultipleJGitEnvironmentRepository; +import org.springframework.cloud.config.server.NativeEnvironmentRepository; +import org.springframework.cloud.config.server.SvnKitEnvironmentRepository; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Profile; diff --git a/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/encryption/EncryptionAutoConfiguration.java b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/config/SingleEncryptorAutoConfiguration.java similarity index 56% rename from spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/encryption/EncryptionAutoConfiguration.java rename to spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/config/SingleEncryptorAutoConfiguration.java index 8b538662..0ad0c64d 100644 --- a/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/encryption/EncryptionAutoConfiguration.java +++ b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/config/SingleEncryptorAutoConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * Copyright 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. @@ -14,36 +14,28 @@ * limitations under the License. */ -package org.springframework.cloud.config.server.encryption; +package org.springframework.cloud.config.server.config; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.AutoConfigureAfter; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.cloud.config.server.encryption.SingleTextEncryptorLocator; +import org.springframework.cloud.config.server.encryption.TextEncryptorLocator; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.springframework.security.crypto.encrypt.Encryptors; import org.springframework.security.crypto.encrypt.TextEncryptor; -/** - * @author Bartosz Wojtkiewicz - * @author Rafal Zukowski - * - */ @Configuration -public class EncryptionAutoConfiguration { +@AutoConfigureAfter(EncryptionAutoConfiguration.class) +public class SingleEncryptorAutoConfiguration { + + @Autowired(required = false) + private TextEncryptor encryptor; @Bean @ConditionalOnMissingBean - public TextEncryptorLocator textEncryptorLocator(TextEncryptor encryptor) { - return new SingleTextEncryptorLocator(encryptor); - } - - @ConditionalOnMissingBean(TextEncryptor.class) - protected static class DefaultTextEncryptorConfiguration { - - @Bean - public TextEncryptor nullTextEncryptor() { - return Encryptors.noOpText(); - } - + public TextEncryptorLocator textEncryptorLocator() { + return new SingleTextEncryptorLocator(this.encryptor); } -} +} \ No newline at end of file diff --git a/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/encryption/CipherEnvironmentEncryptor.java b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/encryption/CipherEnvironmentEncryptor.java index b0cd8853..07a2ceeb 100644 --- a/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/encryption/CipherEnvironmentEncryptor.java +++ b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/encryption/CipherEnvironmentEncryptor.java @@ -22,11 +22,9 @@ 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; @@ -43,25 +41,27 @@ public class CipherEnvironmentEncryptor implements EnvironmentEncryptor { private static Log logger = LogFactory.getLog(CipherEnvironmentEncryptor.class); - private final TextEncryptorLocator encryptorLocator; + private final TextEncryptorLocator encryptor; + + private EnvironmentPrefixHelper helper = new EnvironmentPrefixHelper(); @Autowired - public CipherEnvironmentEncryptor(TextEncryptorLocator encryptorLocator) { - this.encryptorLocator = encryptorLocator; + public CipherEnvironmentEncryptor(TextEncryptorLocator encryptor) { + this.encryptor = encryptor; } @Override public Environment decrypt(Environment environment) { - TextEncryptor encryptor = encryptorLocator.locate(environment.getName(), - StringUtils.arrayToCommaDelimitedString(environment.getProfiles())); - return encryptor != null ? decrypt(environment, encryptor) : environment; + return this.encryptor != null ? decrypt(environment, this.encryptor) + : environment; } - private Environment decrypt(Environment environment, TextEncryptor encryptor) { + private Environment decrypt(Environment environment, TextEncryptorLocator encryptor) { Environment result = new Environment(environment.getName(), - environment.getProfiles(), environment.getLabel()); + environment.getProfiles(), environment.getLabel()); for (PropertySource source : environment.getPropertySources()) { - Map map = new LinkedHashMap(source.getSource()); + Map map = new LinkedHashMap( + source.getSource()); for (Map.Entry entry : new LinkedHashSet<>(map.entrySet())) { Object key = entry.getKey(); String name = key.toString(); @@ -69,13 +69,17 @@ public class CipherEnvironmentEncryptor implements EnvironmentEncryptor { if (value.startsWith("{cipher}")) { map.remove(key); try { - value = value == null ? null : encryptor.decrypt(value.substring( - "{cipher}".length())); - } catch (Exception e) { + value = value.substring("{cipher}".length()); + value = encryptor.locate( + this.helper.getEncryptorKeys(name, StringUtils + .arrayToCommaDelimitedString(environment + .getProfiles()), value)).decrypt(this.helper.stripPrefix(value)); + } + catch (Exception e) { value = ""; name = "invalid." + name; - logger.warn("Cannot decrypt key: " + key - + " (" + e.getClass() + ": " + e.getMessage() + ")"); + logger.warn("Cannot decrypt key: " + key + " (" + e.getClass() + + ": " + e.getMessage() + ")"); } map.put(name, value); } diff --git a/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/EncryptionController.java b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/encryption/EncryptionController.java similarity index 61% rename from spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/EncryptionController.java rename to spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/encryption/EncryptionController.java index 29a588ee..9a3965e4 100644 --- a/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/EncryptionController.java +++ b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/encryption/EncryptionController.java @@ -13,42 +13,33 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.cloud.config.server; +package org.springframework.cloud.config.server.encryption; -import java.io.IOException; import java.io.UnsupportedEncodingException; import java.net.URLDecoder; -import java.security.KeyPair; import java.util.Collections; import java.util.HashMap; import java.util.Map; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; -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.config.server.ConfigServerProperties; import org.springframework.cloud.context.encrypt.KeyFormatException; -import org.springframework.core.io.ByteArrayResource; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.security.crypto.codec.Base64; import org.springframework.security.crypto.codec.Hex; import org.springframework.security.crypto.encrypt.TextEncryptor; -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; import org.springframework.web.bind.annotation.RequestMethod; -import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.bind.annotation.RestController; -import org.springframework.web.multipart.MultipartFile; /** * @author Dave Syer @@ -60,77 +51,36 @@ public class EncryptionController { private static Log logger = LogFactory.getLog(EncryptionController.class); - private final TextEncryptorLocator encryptorLocator; + volatile private TextEncryptorLocator encryptor; private final ConfigServerProperties properties; - public EncryptionController(TextEncryptorLocator encryptorLocator, + private EnvironmentPrefixHelper helper = new EnvironmentPrefixHelper(); + + public EncryptionController(TextEncryptorLocator encryptor, ConfigServerProperties configServerProperties) { - this.encryptorLocator = encryptorLocator; + this.encryptor = encryptor; this.properties = configServerProperties; } @RequestMapping(value = "/key", method = RequestMethod.GET) public String getPublicKey() { - TextEncryptor encryptor = locateDefaultTextEncryptor(); + TextEncryptor encryptor = this.encryptor.locate(this.helper.getEncryptorKeys( + "application", "default", "")); if (!(encryptor instanceof RsaKeyHolder)) { throw new KeyNotAvailableException(); } return ((RsaKeyHolder) encryptor).getPublicKey(); } - @RequestMapping(value = "/key", method = RequestMethod.POST, params = { "password" }) - public ResponseEntity> uploadKeyStore( - @RequestParam("file") MultipartFile file, - @RequestParam("password") String password, @RequestParam("alias") String alias) { - - Map body = new HashMap(); - body.put("status", "OK"); - - try { - ByteArrayResource resource = new ByteArrayResource(file.getBytes()); - KeyPair keyPair = new KeyStoreKeyFactory(resource, password.toCharArray()) - .getKeyPair(alias); - RsaSecretEncryptor encryptor = new RsaSecretEncryptor(keyPair); - updateEncryptor(encryptor); - body.put("publicKey", encryptor.getPublicKey()); - } - catch (IOException e) { - throw new KeyFormatException(); - } - logger.info("Key changed to alias=" + alias); - - return new ResponseEntity>(body, HttpStatus.CREATED); - - } - - @RequestMapping(value = "/key", method = RequestMethod.POST, params = { "!password" }) - public ResponseEntity> uploadKey(@RequestBody String data, - @RequestHeader("Content-Type") MediaType type) { - - Map body = new HashMap(); - body.put("status", "OK"); - - 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>(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(); + @RequestMapping(value = "/key/{name}/{profiles}", method = RequestMethod.GET) + public String getPublicKey(@PathVariable String name, @PathVariable String profiles) { + TextEncryptor encryptor = this.encryptor.locate(this.helper.getEncryptorKeys( + name, profiles, "")); + if (!(encryptor instanceof RsaKeyHolder)) { + throw new KeyNotAvailableException(); } + return ((RsaKeyHolder) encryptor).getPublicKey(); } @ExceptionHandler(KeyFormatException.class) @@ -153,7 +103,7 @@ public class EncryptionController { @RequestMapping(value = "encrypt/status", method = RequestMethod.GET) public Map status() { - checkEncryptorInstalled(locateDefaultTextEncryptor()); + checkEncryptorInstalled("application", "default"); return Collections. singletonMap("status", "OK"); } @@ -161,18 +111,20 @@ public class EncryptionController { public String encrypt(@RequestBody String data, @RequestHeader("Content-Type") MediaType type) { - return encrypt(properties.getDefaultApplicationName(), - properties.getDefaultProfile(), data, type); + return encrypt(this.properties.getDefaultApplicationName(), + this.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) { - + checkEncryptorInstalled(name, profiles); try { - TextEncryptor encryptor = checkEncryptorInstalled(encryptorLocator.locate( - name, profiles)); - String encrypted = encryptor.encrypt(stripFormData(data, type, false)); + String input = stripFormData(data, type, false); + Map keys = this.helper + .getEncryptorKeys(name, profiles, input); + String encrypted = this.helper.addPrefix(keys, this.encryptor.locate(keys) + .encrypt(input)); logger.info("Encrypted data"); return encrypted; } @@ -185,18 +137,19 @@ public class EncryptionController { public String decrypt(@RequestBody String data, @RequestHeader("Content-Type") MediaType type) { - return decrypt(properties.getDefaultApplicationName(), - properties.getDefaultProfile(), data, type); + return decrypt(this.properties.getDefaultApplicationName(), + this.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) { - + checkEncryptorInstalled(name, profiles); try { - TextEncryptor encryptor = checkEncryptorInstalled(encryptorLocator.locate( - name, profiles)); - String decrypted = encryptor.decrypt(stripFormData(data, type, true)); + String input = stripFormData(data, type, true); + String decrypted = this.helper.stripPrefix(this.encryptor.locate( + this.helper.getEncryptorKeys(name, profiles, input)).decrypt( + this.helper.stripPrefix(input))); logger.info("Decrypted cipher data"); return decrypted; } @@ -205,16 +158,13 @@ public class EncryptionController { } } - private TextEncryptor checkEncryptorInstalled(TextEncryptor encryptor) { - if (encryptor == null || encryptor.encrypt("FOO").equals("FOO")) { + private void checkEncryptorInstalled(String name, String profiles) { + if (this.encryptor == null + || this.encryptor + .locate(this.helper.getEncryptorKeys(name, profiles, "")) + .encrypt("FOO").equals("FOO")) { throw new KeyNotInstalledException(); } - return encryptor; - } - - private TextEncryptor locateDefaultTextEncryptor() { - return encryptorLocator.locate(properties.getDefaultApplicationName(), - properties.getDefaultProfile()); } private String stripFormData(String data, MediaType type, boolean cipher) { @@ -285,7 +235,3 @@ class KeyNotAvailableException extends RuntimeException { @SuppressWarnings("serial") class InvalidCipherException extends RuntimeException { } - -@SuppressWarnings("serial") -class IncompatibleTextEncryptorLocatorException extends RuntimeException { -} diff --git a/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/encryption/EnvironmentPrefixHelper.java b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/encryption/EnvironmentPrefixHelper.java new file mode 100644 index 00000000..05a621b5 --- /dev/null +++ b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/encryption/EnvironmentPrefixHelper.java @@ -0,0 +1,128 @@ +/* + * Copyright 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.Map; + +import org.springframework.security.crypto.encrypt.TextEncryptor; +import org.springframework.util.StringUtils; + +/** + * Shared helper class for encryption and decryption concerns where the plain text and + * cipher texts can optionally contain prefixes of {name:value} pairs. + * Special treatment is given to the "name" and "profiles" keys, which can be provided by + * the caller, explicitly instead of by the input text strings. This is to support + * independent decryptions using different cryptographic keys for different applications + * and profiles, if needed (this class does not have any crypto features, but it can be + * used by components that do). + * + * @author Dave Syer + * + */ +class EnvironmentPrefixHelper { + + /** + * key for the "profiles" prefix pair (usually a Spring profile or comma separated + * value). + */ + private static final String PROFILES = "profiles"; + + /** + * key for the "name" (Environment name or application name). + */ + private static final String NAME = "name"; + + /** + * If plain text actually starts with text in the form {name:value} + * prefix it with this to signal the start of the plain text. + */ + private static final String ESCAPE = "{plain}"; + + /** + * Extract keys for looking up a {@link TextEncryptor} from the input text in the form + * of a prefix of zero or many {name:value} pairs. The name and profiles + * properties are always added to the keys (replacing any provided in the inputs). + */ + public Map getEncryptorKeys(String name, String profiles, String text) { + + Map keys = new LinkedHashMap(); + + text = removeEnvironmentPrefix(text); + keys.put(NAME, name); + keys.put(PROFILES, profiles); + + if (text.contains(ESCAPE)) { + text = text.substring(0, text.indexOf(ESCAPE)); + } + + String[] tokens = StringUtils.split(text, "}"); + while (tokens != null) { + String token = tokens[0].trim(); + if (token.startsWith("{")) { + String key = ""; + String value = ""; + if (token.contains(":") && !token.endsWith(":")) { + key = token.substring(1, token.indexOf(":")); + value = token.substring(token.indexOf(":") + 1); + } + else { + key = token.substring(1); + } + keys.put(key, value); + } + text = tokens[1]; + tokens = StringUtils.split(text, "}"); + } + + return keys; + + } + + /** + * Add a prefix to the input text (usually a cipher) consisting of the + * {name:value} pairs. The "name" and "profiles" keys are special in that + * they are stripped since that information is always available when deriving the keys + * in {@link #getEncryptorKeys(String, String, String)}. + */ + public String addPrefix(Map keys, String input) { + keys.remove(NAME); + keys.remove(PROFILES); + StringBuilder builder = new StringBuilder(); + for (String key : keys.keySet()) { + builder.append("{").append(key).append(":").append(keys.get(key)).append("}"); + } + builder.append(input); + return builder.toString(); + } + + public String stripPrefix(String value) { + if (!value.contains("}")) { + return value; + } + if (value.contains(ESCAPE)) { + return value.substring(value.indexOf(ESCAPE) + ESCAPE.length()); + } + return value.substring(value.lastIndexOf("}") + 1); + } + + private String removeEnvironmentPrefix(String input) { + return input.replaceFirst("\\{name:.*\\}", "").replaceFirst("\\{profiles:.*\\}", + ""); + } + +} diff --git a/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/encryption/KeyStoreTextEncryptorLocator.java b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/encryption/KeyStoreTextEncryptorLocator.java new file mode 100644 index 00000000..7ef44053 --- /dev/null +++ b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/encryption/KeyStoreTextEncryptorLocator.java @@ -0,0 +1,71 @@ +/* + * Copyright 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.Map; + +import org.springframework.security.crypto.encrypt.TextEncryptor; +import org.springframework.security.rsa.crypto.KeyStoreKeyFactory; +import org.springframework.security.rsa.crypto.RsaSecretEncryptor; + +/** + * A {@link TextEncryptorLocator} that pulls RSA key pairs out of a keystore. The input + * map can contain entries for "key" or "secret" or both, or neither. The secret in the + * input map is not, in general, the secret in the keystore, but is dereferenced through a + * {@link SecretLocator} (so for example you can keep a table of encrypted secrets and + * update it separately to the keystore). + * + * @author Dave Syer + * + */ +public class KeyStoreTextEncryptorLocator implements TextEncryptorLocator { + + private final static String KEY = "key"; + + private final static String SECRET = "secret"; + + private KeyStoreKeyFactory keys; + + private String defaultSecret; + + private String defaultAlias; + + private SecretLocator secretLocator = new PassthruSecretLocator(); + + public KeyStoreTextEncryptorLocator(KeyStoreKeyFactory keys, String defaultSecret, + String defaultAlias) { + this.keys = keys; + this.defaultAlias = defaultAlias; + this.defaultSecret = defaultSecret; + } + + /** + * @param secretLocator the secretLocator to set + */ + public void setSecretLocator(SecretLocator secretLocator) { + this.secretLocator = secretLocator; + } + + @Override + public TextEncryptor locate(Map keys) { + String alias = keys.containsKey(KEY) ? keys.get(KEY) : this.defaultAlias; + String secret = keys.containsKey(SECRET) ? keys.get(SECRET) : this.defaultSecret; + return new RsaSecretEncryptor(this.keys.getKeyPair(alias, + this.secretLocator.locate(secret))); + } + +} diff --git a/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/encryption/PassthruSecretLocator.java b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/encryption/PassthruSecretLocator.java new file mode 100644 index 00000000..af2a24d3 --- /dev/null +++ b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/encryption/PassthruSecretLocator.java @@ -0,0 +1,30 @@ +/* + * Copyright 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; + +/** + * @author Dave Syer + * + */ +public class PassthruSecretLocator implements SecretLocator { + + @Override + public char[] locate(String secret) { + return secret==null ? new char[0] : secret.toCharArray(); + } + +} diff --git a/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/encryption/SecretLocator.java b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/encryption/SecretLocator.java new file mode 100644 index 00000000..9e543fa3 --- /dev/null +++ b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/encryption/SecretLocator.java @@ -0,0 +1,27 @@ +/* + * Copyright 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; + +/** + * @author Dave Syer + * + */ +public interface SecretLocator { + + char[] locate(String secret); + +} diff --git a/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/encryption/SingleTextEncryptorLocator.java b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/encryption/SingleTextEncryptorLocator.java index c7db7471..210bf344 100644 --- a/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/encryption/SingleTextEncryptorLocator.java +++ b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/encryption/SingleTextEncryptorLocator.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * Copyright 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. @@ -16,33 +16,27 @@ package org.springframework.cloud.config.server.encryption; +import java.util.Map; + +import org.springframework.security.crypto.encrypt.Encryptors; 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 + * @author Dave Syer * */ 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; + this.encryptor = encryptor == null ? Encryptors.noOpText() : encryptor; } @Override - public TextEncryptor locate(String applicationName, String profiles) { - return encryptor; + public TextEncryptor locate(Map keys) { + return this.encryptor; } + } diff --git a/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/encryption/TextEncryptorLocator.java b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/encryption/TextEncryptorLocator.java index 2b2cd456..b4b2134a 100644 --- a/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/encryption/TextEncryptorLocator.java +++ b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/encryption/TextEncryptorLocator.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * Copyright 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. @@ -16,22 +16,17 @@ package org.springframework.cloud.config.server.encryption; +import java.util.Map; + 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 Dave Syer * @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); + + TextEncryptor locate(Map keys); + } diff --git a/spring-cloud-config-server/src/main/resources/META-INF/spring.factories b/spring-cloud-config-server/src/main/resources/META-INF/spring.factories index 0bcdc3ea..88154389 100644 --- a/spring-cloud-config-server/src/main/resources/META-INF/spring.factories +++ b/spring-cloud-config-server/src/main/resources/META-INF/spring.factories @@ -8,4 +8,5 @@ org.springframework.cloud.config.server.ConfigServerBootstrapApplicationListener # Autoconfiguration org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ -org.springframework.cloud.config.server.encryption.EncryptionAutoConfiguration +org.springframework.cloud.config.server.config.EncryptionAutoConfiguration,\ +org.springframework.cloud.config.server.config.SingleEncryptorAutoConfiguration diff --git a/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/EncryptionControllerMultiTextEncryptorTests.java b/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/EncryptionControllerMultiTextEncryptorTests.java deleted file mode 100644 index 8233a40c..00000000 --- a/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/EncryptionControllerMultiTextEncryptorTests.java +++ /dev/null @@ -1,77 +0,0 @@ -package org.springframework.cloud.config.server; - -import java.util.HashMap; -import java.util.Map; - -import org.junit.Test; - -import org.springframework.cloud.config.server.encryption.TextEncryptorLocator; -import org.springframework.security.crypto.encrypt.TextEncryptor; - -import static org.junit.Assert.assertEquals; -import static org.springframework.http.MediaType.TEXT_PLAIN; -import static org.springframework.security.crypto.encrypt.Encryptors.noOpText; -import static org.springframework.security.crypto.encrypt.Encryptors.text; - -/** - * @author Bartosz Wojtkiewicz - * - */ - -public class EncryptionControllerMultiTextEncryptorTests { - NaiveMultiTextEncryptorLocator encryptorLocator = new NaiveMultiTextEncryptorLocator(); - ConfigServerProperties properties = new ConfigServerProperties(); - EncryptionController controller = new EncryptionController(encryptorLocator, properties); - - String application = "application"; - String profiles = "profile1,profile2"; - String data = "foo"; - - @Test - public void shouldEncryptUsingApplicationAndProfiles() { - // given - encryptorLocator.addSupportFor(application, profiles); - - // when - String encrypted = controller.encrypt(application, profiles, data, TEXT_PLAIN); - - // then - assertEquals(data, controller.decrypt(application, profiles, encrypted, TEXT_PLAIN)); - } - - @Test(expected = KeyNotInstalledException.class) - public void shouldNotEncryptUsingUnknownApplicationName() { - // given - String application = "unknown"; - - // when - controller.encrypt(application, profiles, data, TEXT_PLAIN); - - // then exception is thrown - } - - @Test(expected = KeyNotInstalledException.class) - public void shouldNotDecryptUsingUnknownApplicationName() { - // given - String application = "unknown"; - - // when - controller.decrypt(application, profiles, data, TEXT_PLAIN); - - // then exception is thrown - } - - class NaiveMultiTextEncryptorLocator implements TextEncryptorLocator { - private Map encryptors = new HashMap<>(); - - @Override - public TextEncryptor locate(String applicationName, String profiles) { - String key = applicationName + profiles; - return encryptors.containsKey(key) ? encryptors.get(key) : noOpText(); - } - - public void addSupportFor(String applicationName, String profiles) { - encryptors.put(applicationName + profiles, text(applicationName, "11")); - } - } -} diff --git a/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/EncryptionControllerTests.java b/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/EncryptionControllerTests.java deleted file mode 100644 index 11be3bde..00000000 --- a/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/EncryptionControllerTests.java +++ /dev/null @@ -1,103 +0,0 @@ -/* - * Copyright 2013-2014 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; - -import org.junit.Test; -import org.springframework.cloud.config.server.encryption.SingleTextEncryptorLocator; -import org.springframework.cloud.context.encrypt.KeyFormatException; -import org.springframework.http.MediaType; -import org.springframework.security.crypto.encrypt.Encryptors; -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 SingleTextEncryptorLocator textEncryptorLocator = new SingleTextEncryptorLocator(); - private ConfigServerProperties properties = new ConfigServerProperties(); - private EncryptionController controller = new EncryptionController(textEncryptorLocator, properties); - - @Test(expected = KeyNotInstalledException.class) - public void cannotDecryptWithoutKey() { - controller.decrypt("foo", MediaType.TEXT_PLAIN); - } - - @Test(expected = KeyNotInstalledException.class) - public void cannotDecryptWithNoopEncryptor() { - textEncryptorLocator.setEncryptor(Encryptors.noOpText()); - controller.decrypt("foo", MediaType.TEXT_PLAIN); - } - - @Test(expected = KeyFormatException.class) - public void cannotUploadPublicKey() { - controller.uploadKey("ssh-rsa ...", MediaType.TEXT_PLAIN); - } - - @Test(expected = KeyFormatException.class) - public void cannotUploadPublicKeyPemFormat() { - controller.uploadKey("---- BEGIN RSA PUBLIC KEY ...", MediaType.TEXT_PLAIN); - } - - @Test(expected = InvalidCipherException.class) - public void invalidCipher() { - controller.uploadKey("foo", MediaType.TEXT_PLAIN); - controller.decrypt("foo", MediaType.TEXT_PLAIN); - } - - @Test - public void sunnyDaySymmetricKey() { - controller.uploadKey("foo", MediaType.TEXT_PLAIN); - String cipher = controller.encrypt("foo", MediaType.TEXT_PLAIN); - assertEquals("foo", controller.decrypt(cipher, MediaType.TEXT_PLAIN)); - } - - @Test - public void sunnyDayRsaKey() { - textEncryptorLocator.setEncryptor(new RsaSecretEncryptor()); - String cipher = controller.encrypt("foo", MediaType.TEXT_PLAIN); - assertEquals("foo", controller.decrypt(cipher, MediaType.TEXT_PLAIN)); - } - - @Test - public void publicKey() { - textEncryptorLocator.setEncryptor(new RsaSecretEncryptor()); - String key = controller.getPublicKey(); - assertTrue("Wrong key format: " + key, key.startsWith("ssh-rsa")); - } - - @Test - public void formDataIn() { - 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 randomizedCipher() { - controller.uploadKey("foo", MediaType.TEXT_PLAIN); - String cipher = controller.encrypt("foo", MediaType.TEXT_PLAIN); - assertNotEquals(cipher, controller.encrypt("foo", MediaType.TEXT_PLAIN)); - } - -} diff --git a/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/EnvironmentControllerIntegrationTests.java b/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/EnvironmentControllerIntegrationTests.java index 4a98a570..15e09ccc 100644 --- a/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/EnvironmentControllerIntegrationTests.java +++ b/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/EnvironmentControllerIntegrationTests.java @@ -19,10 +19,8 @@ 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; @@ -39,32 +37,33 @@ public class EnvironmentControllerIntegrationTests { @Before public void init() { - Mockito.when(repository.getDefaultLabel()).thenReturn("master"); - mvc = MockMvcBuilders.standaloneSetup( - new EnvironmentController(repository, new CipherEnvironmentEncryptor(new SingleTextEncryptorLocator()))) - .build(); + Mockito.when(this.repository.getDefaultLabel()).thenReturn("master"); + this.mvc = MockMvcBuilders.standaloneSetup( + new EnvironmentController(this.repository, + new CipherEnvironmentEncryptor(null))).build(); } @Test public void environmentNoLabel() throws Exception { - Mockito.when(repository.findOne("foo", "default", "master")).thenReturn( + Mockito.when(this.repository.findOne("foo", "default", "master")).thenReturn( new Environment("foo", "default")); - mvc.perform(MockMvcRequestBuilders.get("/foo/default")).andExpect( + this.mvc.perform(MockMvcRequestBuilders.get("/foo/default")).andExpect( MockMvcResultMatchers.status().isOk()); } @Test public void environmentWithLabel() throws Exception { - Mockito.when(repository.findOne("foo", "default", "awesome")).thenReturn( + Mockito.when(this.repository.findOne("foo", "default", "awesome")).thenReturn( new Environment("foo", "default")); - mvc.perform(MockMvcRequestBuilders.get("/foo/default/awesome")).andExpect( + this.mvc.perform(MockMvcRequestBuilders.get("/foo/default/awesome")).andExpect( MockMvcResultMatchers.status().isOk()); } + @Test public void environmentWithLabelContainingPeriod() throws Exception { - Mockito.when(repository.findOne("foo", "default", "1.0.0")).thenReturn( + Mockito.when(this.repository.findOne("foo", "default", "1.0.0")).thenReturn( new Environment("foo", "default")); - mvc.perform(MockMvcRequestBuilders.get("/foo/default/1.0.0")).andExpect( + this.mvc.perform(MockMvcRequestBuilders.get("/foo/default/1.0.0")).andExpect( MockMvcResultMatchers.status().isOk()); } diff --git a/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/EnvironmentControllerTests.java b/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/EnvironmentControllerTests.java index edfdadc3..7b49ed39 100644 --- a/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/EnvironmentControllerTests.java +++ b/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/EnvironmentControllerTests.java @@ -15,6 +15,9 @@ */ package org.springframework.cloud.config.server; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + import java.util.Collections; import java.util.HashMap; import java.util.LinkedHashMap; @@ -25,20 +28,15 @@ import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; 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; import org.springframework.test.web.servlet.result.MockMvcResultMatchers; import org.springframework.test.web.servlet.setup.MockMvcBuilders; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertTrue; - /** * @author Dave Syer * @author Roy Clarkson @@ -56,17 +54,18 @@ public class EnvironmentControllerTests { @Before public void init() { - Mockito.when(repository.getDefaultLabel()).thenReturn("master"); - this.controller = new EnvironmentController(repository, new CipherEnvironmentEncryptor(new SingleTextEncryptorLocator())); + Mockito.when(this.repository.getDefaultLabel()).thenReturn("master"); + this.controller = new EnvironmentController(this.repository, + new CipherEnvironmentEncryptor(null)); } @Test public void vanillaYaml() throws Exception { Map map = new HashMap(); map.put("a.b.c", "d"); - environment.add(new PropertySource("one", map)); - Mockito.when(repository.findOne("foo", "bar", "master")).thenReturn(environment); - String yaml = controller.yaml("foo", "bar").getBody(); + this.environment.add(new PropertySource("one", map)); + Mockito.when(this.repository.findOne("foo", "bar", "master")).thenReturn(this.environment); + String yaml = this.controller.yaml("foo", "bar").getBody(); assertEquals("a:\n b:\n c: d\n", yaml); } @@ -74,11 +73,11 @@ public class EnvironmentControllerTests { public void propertyOverrideInYaml() throws Exception { Map map = new LinkedHashMap(); map.put("a.b.c", "d"); - environment.add(new PropertySource("one", map)); - environment.addFirst(new PropertySource("two", Collections.singletonMap("a.b.c", + this.environment.add(new PropertySource("one", map)); + this.environment.addFirst(new PropertySource("two", Collections.singletonMap("a.b.c", "e"))); - Mockito.when(repository.findOne("foo", "bar", "master")).thenReturn(environment); - String yaml = controller.yaml("foo", "bar").getBody(); + Mockito.when(this.repository.findOne("foo", "bar", "master")).thenReturn(this.environment); + String yaml = this.controller.yaml("foo", "bar").getBody(); assertEquals("a:\n b:\n c: e\n", yaml); } @@ -87,9 +86,9 @@ public class EnvironmentControllerTests { Map map = new LinkedHashMap(); map.put("a.b[0]", "c"); map.put("a.b[1]", "d"); - environment.add(new PropertySource("one", map)); - Mockito.when(repository.findOne("foo", "bar", "master")).thenReturn(environment); - String yaml = controller.yaml("foo", "bar").getBody(); + this.environment.add(new PropertySource("one", map)); + Mockito.when(this.repository.findOne("foo", "bar", "master")).thenReturn(this.environment); + String yaml = this.controller.yaml("foo", "bar").getBody(); assertEquals("a:\n b:\n - c\n - d\n", yaml); } @@ -97,9 +96,9 @@ public class EnvironmentControllerTests { public void textAtTopLevelInYaml() throws Exception { Map map = new LinkedHashMap(); map.put("document", "blah"); - environment.add(new PropertySource("one", map)); - Mockito.when(repository.findOne("foo", "bar", "master")).thenReturn(environment); - String yaml = controller.yaml("foo", "bar").getBody(); + this.environment.add(new PropertySource("one", map)); + Mockito.when(this.repository.findOne("foo", "bar", "master")).thenReturn(this.environment); + String yaml = this.controller.yaml("foo", "bar").getBody(); assertEquals("blah\n", yaml); } @@ -108,9 +107,9 @@ public class EnvironmentControllerTests { Map map = new LinkedHashMap(); map.put("document[0]", "c"); map.put("document[1]", "d"); - environment.add(new PropertySource("one", map)); - Mockito.when(repository.findOne("foo", "bar", "master")).thenReturn(environment); - String yaml = controller.yaml("foo", "bar").getBody(); + this.environment.add(new PropertySource("one", map)); + Mockito.when(this.repository.findOne("foo", "bar", "master")).thenReturn(this.environment); + String yaml = this.controller.yaml("foo", "bar").getBody(); assertEquals("- c\n- d\n", yaml); } @@ -119,9 +118,9 @@ public class EnvironmentControllerTests { Map map = new LinkedHashMap(); map.put("document[0].a", "c"); map.put("document[1].a", "d"); - environment.add(new PropertySource("one", map)); - Mockito.when(repository.findOne("foo", "bar", "master")).thenReturn(environment); - String yaml = controller.yaml("foo", "bar").getBody(); + this.environment.add(new PropertySource("one", map)); + Mockito.when(this.repository.findOne("foo", "bar", "master")).thenReturn(this.environment); + String yaml = this.controller.yaml("foo", "bar").getBody(); assertEquals("- a: c\n- a: d\n", yaml); } @@ -131,12 +130,12 @@ public class EnvironmentControllerTests { map.put("a.b[0].c", "d"); map.put("a.b[0].d", "e"); map.put("a.b[1].c", "d"); - environment.add(new PropertySource("one", map)); - Mockito.when(repository.findOne("foo", "bar", "master")).thenReturn(environment); - String yaml = controller.yaml("foo", "bar").getBody(); + this.environment.add(new PropertySource("one", map)); + Mockito.when(this.repository.findOne("foo", "bar", "master")).thenReturn(this.environment); + String yaml = this.controller.yaml("foo", "bar").getBody(); assertTrue("Wrong output: " + yaml, "a:\n b:\n - d: e\n c: d\n - c: d\n".equals(yaml) - || "a:\n b:\n - c: d\n d: e\n - c: d\n".equals(yaml)); + || "a:\n b:\n - c: d\n d: e\n - c: d\n".equals(yaml)); } @Test @@ -144,9 +143,9 @@ public class EnvironmentControllerTests { Map map = new LinkedHashMap(); map.put("b[0].c", "d"); map.put("b[1].c", "d"); - environment.add(new PropertySource("one", map)); - Mockito.when(repository.findOne("foo", "bar", "master")).thenReturn(environment); - String yaml = controller.yaml("foo", "bar").getBody(); + this.environment.add(new PropertySource("one", map)); + Mockito.when(this.repository.findOne("foo", "bar", "master")).thenReturn(this.environment); + String yaml = this.controller.yaml("foo", "bar").getBody(); assertEquals("b:\n- c: d\n- c: d\n", yaml); } @@ -155,128 +154,128 @@ public class EnvironmentControllerTests { Map map = new LinkedHashMap(); map.put("x.a.b[0].c", "d"); map.put("x.a.b[1].c", "d"); - environment.add(new PropertySource("one", map)); - Mockito.when(repository.findOne("foo", "bar", "master")).thenReturn(environment); - String yaml = controller.yaml("foo", "bar").getBody(); + this.environment.add(new PropertySource("one", map)); + Mockito.when(this.repository.findOne("foo", "bar", "master")).thenReturn(this.environment); + String yaml = this.controller.yaml("foo", "bar").getBody(); assertEquals("x:\n a:\n b:\n - c: d\n - c: d\n", yaml); } @Test public void mappingForEnvironment() throws Exception { - Mockito.when(repository.findOne("foo", "bar", "master")).thenReturn(environment); - MockMvc mvc = MockMvcBuilders.standaloneSetup(controller).build(); + Mockito.when(this.repository.findOne("foo", "bar", "master")).thenReturn(this.environment); + MockMvc mvc = MockMvcBuilders.standaloneSetup(this.controller).build(); mvc.perform(MockMvcRequestBuilders.get("/foo/bar")).andExpect( MockMvcResultMatchers.status().isOk()); } @Test public void mappingForLabelledEnvironment() throws Exception { - Mockito.when(repository.findOne("foo", "bar", "other")).thenReturn(environment); - MockMvc mvc = MockMvcBuilders.standaloneSetup(controller).build(); + Mockito.when(this.repository.findOne("foo", "bar", "other")).thenReturn(this.environment); + MockMvc mvc = MockMvcBuilders.standaloneSetup(this.controller).build(); mvc.perform(MockMvcRequestBuilders.get("/foo/bar/other")).andExpect( MockMvcResultMatchers.status().isOk()); } @Test public void mappingForYaml() throws Exception { - Mockito.when(repository.findOne("foo", "bar", "master")).thenReturn(environment); - MockMvc mvc = MockMvcBuilders.standaloneSetup(controller).build(); + Mockito.when(this.repository.findOne("foo", "bar", "master")).thenReturn(this.environment); + MockMvc mvc = MockMvcBuilders.standaloneSetup(this.controller).build(); mvc.perform(MockMvcRequestBuilders.get("/foo-bar.yml")) - .andExpect( - MockMvcResultMatchers.content().contentType(MediaType.TEXT_PLAIN)) + .andExpect( + MockMvcResultMatchers.content().contentType(MediaType.TEXT_PLAIN)) .andExpect(MockMvcResultMatchers.content().string("{}\n")); } @Test public void mappingForJson() throws Exception { - Mockito.when(repository.findOne("foo", "bar", "master")).thenReturn(environment); - MockMvc mvc = MockMvcBuilders.standaloneSetup(controller).build(); + Mockito.when(this.repository.findOne("foo", "bar", "master")).thenReturn(this.environment); + MockMvc mvc = MockMvcBuilders.standaloneSetup(this.controller).build(); mvc.perform(MockMvcRequestBuilders.get("/foo-bar.json")) - .andExpect( - MockMvcResultMatchers.content().contentType( - MediaType.APPLICATION_JSON)) - .andExpect(MockMvcResultMatchers.content().string("{}")); + .andExpect( + MockMvcResultMatchers.content().contentType( + MediaType.APPLICATION_JSON)) + .andExpect(MockMvcResultMatchers.content().string("{}")); ; } @Test public void mappingForLabelledYaml() throws Exception { - Mockito.when(repository.findOne("foo", "bar", "other")).thenReturn(environment); - MockMvc mvc = MockMvcBuilders.standaloneSetup(controller).build(); + Mockito.when(this.repository.findOne("foo", "bar", "other")).thenReturn(this.environment); + MockMvc mvc = MockMvcBuilders.standaloneSetup(this.controller).build(); mvc.perform(MockMvcRequestBuilders.get("/other/foo-bar.yml")).andExpect( MockMvcResultMatchers.content().contentType(MediaType.TEXT_PLAIN)); } @Test public void mappingForLabelledProperties() throws Exception { - Mockito.when(repository.findOne("foo", "bar", "other")).thenReturn(environment); - MockMvc mvc = MockMvcBuilders.standaloneSetup(controller).build(); + Mockito.when(this.repository.findOne("foo", "bar", "other")).thenReturn(this.environment); + MockMvc mvc = MockMvcBuilders.standaloneSetup(this.controller).build(); mvc.perform(MockMvcRequestBuilders.get("/other/foo-bar.properties")).andExpect( MockMvcResultMatchers.content().contentType(MediaType.TEXT_PLAIN)); } @Test public void mappingForProperties() throws Exception { - Mockito.when(repository.findOne("foo", "bar", "master")).thenReturn(environment); - MockMvc mvc = MockMvcBuilders.standaloneSetup(controller).build(); + Mockito.when(this.repository.findOne("foo", "bar", "master")).thenReturn(this.environment); + MockMvc mvc = MockMvcBuilders.standaloneSetup(this.controller).build(); mvc.perform(MockMvcRequestBuilders.get("/foo-bar.properties")).andExpect( MockMvcResultMatchers.content().contentType(MediaType.TEXT_PLAIN)); } @Test public void mappingForLabelledYamlWithHyphen() throws Exception { - Mockito.when(repository.findOne("foo", "bar-spam", "other")).thenReturn( - environment); - MockMvc mvc = MockMvcBuilders.standaloneSetup(controller).build(); + Mockito.when(this.repository.findOne("foo", "bar-spam", "other")).thenReturn( + this.environment); + MockMvc mvc = MockMvcBuilders.standaloneSetup(this.controller).build(); mvc.perform(MockMvcRequestBuilders.get("/other/foo-bar-spam.yml")).andExpect( MockMvcResultMatchers.status().isBadRequest()); } @Test public void mappingforLabelledJsonProperties() throws Exception { - Mockito.when(repository.findOne("foo", "bar", "other")).thenReturn(environment); - MockMvc mvc = MockMvcBuilders.standaloneSetup(controller).build(); + Mockito.when(this.repository.findOne("foo", "bar", "other")).thenReturn(this.environment); + MockMvc mvc = MockMvcBuilders.standaloneSetup(this.controller).build(); mvc.perform(MockMvcRequestBuilders.get("/other/foo-bar.json")).andExpect( MockMvcResultMatchers.content().contentType(MediaType.APPLICATION_JSON)); } @Test public void mappingforJsonProperties() throws Exception { - Mockito.when(repository.findOne("foo", "bar", "master")).thenReturn(environment); - MockMvc mvc = MockMvcBuilders.standaloneSetup(controller).build(); + Mockito.when(this.repository.findOne("foo", "bar", "master")).thenReturn(this.environment); + MockMvc mvc = MockMvcBuilders.standaloneSetup(this.controller).build(); mvc.perform(MockMvcRequestBuilders.get("/foo-bar.json")).andExpect( MockMvcResultMatchers.content().contentType(MediaType.APPLICATION_JSON)); } @Test public void mappingForLabelledJsonPropertiesWithHyphen() throws Exception { - Mockito.when(repository.findOne("foo", "bar-spam", "other")).thenReturn( - environment); - MockMvc mvc = MockMvcBuilders.standaloneSetup(controller).build(); + Mockito.when(this.repository.findOne("foo", "bar-spam", "other")).thenReturn( + this.environment); + MockMvc mvc = MockMvcBuilders.standaloneSetup(this.controller).build(); mvc.perform(MockMvcRequestBuilders.get("/other/foo-bar-spam.json")).andExpect( MockMvcResultMatchers.status().isBadRequest()); } @Test public void allowOverrideFalse() throws Exception { - controller.setOverrides(Collections.singletonMap("foo", "bar")); + this.controller.setOverrides(Collections.singletonMap("foo", "bar")); Map map = new HashMap(); map.put("a.b.c", "d"); - environment.add(new PropertySource("one", map)); - Mockito.when(repository.findOne("foo", "bar", "master")).thenReturn(environment); - assertEquals("{foo=bar}", controller.defaultLabel("foo", "bar").getPropertySources() - .get(0).getSource().toString()); + this.environment.add(new PropertySource("one", map)); + Mockito.when(this.repository.findOne("foo", "bar", "master")).thenReturn(this.environment); + assertEquals("{foo=bar}", this.controller.defaultLabel("foo", "bar") + .getPropertySources().get(0).getSource().toString()); } @Test public void overrideWithEscapedPlaceholders() throws Exception { - controller.setOverrides(Collections.singletonMap("foo", "$\\{bar}")); + this.controller.setOverrides(Collections.singletonMap("foo", "$\\{bar}")); Map map = new HashMap(); map.put("bar", "foo"); - environment.add(new PropertySource("one", map)); - Mockito.when(repository.findOne("foo", "bar", "master")).thenReturn(environment); - assertEquals("{foo=${bar}}", controller.defaultLabel("foo", "bar").getPropertySources() - .get(0).getSource().toString()); + this.environment.add(new PropertySource("one", map)); + Mockito.when(this.repository.findOne("foo", "bar", "master")).thenReturn(this.environment); + assertEquals("{foo=${bar}}", this.controller.defaultLabel("foo", "bar") + .getPropertySources().get(0).getSource().toString()); } } diff --git a/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/JGitEnvironmentRepositoryIntegrationTests.java b/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/JGitEnvironmentRepositoryIntegrationTests.java index 73559472..e1db550a 100644 --- a/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/JGitEnvironmentRepositoryIntegrationTests.java +++ b/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/JGitEnvironmentRepositoryIntegrationTests.java @@ -33,6 +33,7 @@ import org.junit.Test; import org.springframework.boot.autoconfigure.PropertyPlaceholderAutoConfiguration; import org.springframework.boot.builder.SpringApplicationBuilder; import org.springframework.cloud.config.environment.Environment; +import org.springframework.cloud.config.server.config.EnvironmentRepositoryConfiguration; import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; diff --git a/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/MultipleJGitEnvironmentRepositoryIntegrationTests.java b/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/MultipleJGitEnvironmentRepositoryIntegrationTests.java index 2289e724..d4b6a279 100644 --- a/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/MultipleJGitEnvironmentRepositoryIntegrationTests.java +++ b/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/MultipleJGitEnvironmentRepositoryIntegrationTests.java @@ -30,9 +30,9 @@ import org.junit.Test; import org.springframework.boot.autoconfigure.PropertyPlaceholderAutoConfiguration; import org.springframework.boot.builder.SpringApplicationBuilder; import org.springframework.cloud.config.environment.Environment; -import org.springframework.cloud.config.server.EnvironmentRepositoryConfiguration; import org.springframework.cloud.config.server.ConfigServerTestUtils; import org.springframework.cloud.config.server.EnvironmentRepository; +import org.springframework.cloud.config.server.config.EnvironmentRepositoryConfiguration; import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; diff --git a/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/SVNKitEnvironmentRepositoryIntegrationTests.java b/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/SVNKitEnvironmentRepositoryIntegrationTests.java index 10b5d3f7..c59c03b8 100644 --- a/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/SVNKitEnvironmentRepositoryIntegrationTests.java +++ b/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/SVNKitEnvironmentRepositoryIntegrationTests.java @@ -31,10 +31,10 @@ import org.tmatesoft.svn.core.wc2.SvnCheckout; import org.tmatesoft.svn.core.wc2.SvnCommit; import org.tmatesoft.svn.core.wc2.SvnOperationFactory; import org.tmatesoft.svn.core.wc2.SvnTarget; - import org.springframework.boot.autoconfigure.PropertyPlaceholderAutoConfiguration; import org.springframework.boot.builder.SpringApplicationBuilder; import org.springframework.cloud.config.environment.Environment; +import org.springframework.cloud.config.server.config.EnvironmentRepositoryConfiguration; import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; diff --git a/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/encryption/CipherEnvironmentEncryptorTests.java b/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/encryption/CipherEnvironmentEncryptorTests.java index c76a94db..3c191b47 100644 --- a/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/encryption/CipherEnvironmentEncryptorTests.java +++ b/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/encryption/CipherEnvironmentEncryptorTests.java @@ -16,34 +16,54 @@ package org.springframework.cloud.config.server.encryption; +import static java.util.UUID.randomUUID; +import static org.junit.Assert.assertEquals; + import java.util.Collections; +import java.util.Map; 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); + TextEncryptor textEncryptor = new EncryptorFactory().create("foo"); + EnvironmentEncryptor encryptor = new CipherEnvironmentEncryptor(new TextEncryptorLocator() { - @Test - public void shouldDecryptEnvironment() { - // given - String secret = randomUUID().toString(); + @Override + public TextEncryptor locate(Map keys) { + return CipherEnvironmentEncryptorTests.this.textEncryptor; + } + }); - // when - Environment environment = new Environment("name", "profile", "label"); - environment.add(new PropertySource("a", - Collections.singletonMap(environment.getName(), "{cipher}" + textEncryptor.encrypt(secret)))); + @Test + public void shouldDecryptEnvironment() { + // given + String secret = randomUUID().toString(); + + // when + Environment environment = new Environment("name", "profile", "label"); + environment.add(new PropertySource("a", + Collections.singletonMap(environment.getName(), "{cipher}" + this.textEncryptor.encrypt(secret)))); + + // then + assertEquals(secret, this.encryptor.decrypt(environment).getPropertySources().get(0).getSource().get(environment.getName())); + } + + @Test + public void shouldDecryptEnvironmentWithKey() { + // given + String secret = randomUUID().toString(); + + // when + Environment environment = new Environment("name", "profile", "label"); + environment.add(new PropertySource("a", + Collections.singletonMap(environment.getName(), "{cipher}{key:test}" + this.textEncryptor.encrypt(secret)))); + + // then + assertEquals(secret, this.encryptor.decrypt(environment).getPropertySources().get(0).getSource().get(environment.getName())); + } - // then - assertEquals(secret, encryptor.decrypt(environment).getPropertySources().get(0).getSource().get(environment.getName())); - } } diff --git a/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/encryption/EncryptionControllerMultiTextEncryptorTests.java b/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/encryption/EncryptionControllerMultiTextEncryptorTests.java new file mode 100644 index 00000000..6be3f1ce --- /dev/null +++ b/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/encryption/EncryptionControllerMultiTextEncryptorTests.java @@ -0,0 +1,62 @@ +package org.springframework.cloud.config.server.encryption; + +import static org.junit.Assert.assertEquals; +import static org.springframework.http.MediaType.TEXT_PLAIN; + +import org.junit.Test; +import org.springframework.cloud.config.server.ConfigServerProperties; +import org.springframework.security.crypto.encrypt.Encryptors; + +/** + * @author Bartosz Wojtkiewicz + * + */ + +public class EncryptionControllerMultiTextEncryptorTests { + + ConfigServerProperties properties = new ConfigServerProperties(); + EncryptionController controller = new EncryptionController( + new SingleTextEncryptorLocator(Encryptors.noOpText()), this.properties); + + String application = "application"; + String profiles = "profile1,profile2"; + String data = "foo"; + + @Test + public void shouldEncryptUsingApplicationAndProfiles() { + + this.controller = new EncryptionController(new SingleTextEncryptorLocator( + Encryptors.text("application", "11")), this.properties); + + // when + String encrypted = this.controller.encrypt(this.application, this.profiles, + this.data, TEXT_PLAIN); + + // then + assertEquals(this.data, this.controller.decrypt(this.application, this.profiles, + encrypted, TEXT_PLAIN)); + } + + @Test(expected = KeyNotInstalledException.class) + public void shouldNotEncryptUsingNoOp() { + // given + String application = "unknown"; + + // when + this.controller.encrypt(application, this.profiles, this.data, TEXT_PLAIN); + + // then exception is thrown + } + + @Test(expected = KeyNotInstalledException.class) + public void shouldNotDecryptUsingNoOp() { + // given + String application = "unknown"; + + // when + this.controller.decrypt(application, this.profiles, this.data, TEXT_PLAIN); + + // then exception is thrown + } + +} diff --git a/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/encryption/EncryptionControllerTests.java b/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/encryption/EncryptionControllerTests.java new file mode 100644 index 00000000..1e2f340a --- /dev/null +++ b/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/encryption/EncryptionControllerTests.java @@ -0,0 +1,124 @@ +/* + * Copyright 2013-2014 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 static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import java.util.Map; + +import org.junit.Test; +import org.springframework.cloud.config.server.ConfigServerProperties; +import org.springframework.http.MediaType; +import org.springframework.security.crypto.encrypt.Encryptors; +import org.springframework.security.crypto.encrypt.TextEncryptor; +import org.springframework.security.rsa.crypto.RsaSecretEncryptor; + +/** + * @author Dave Syer + * + */ +public class EncryptionControllerTests { + + private ConfigServerProperties properties = new ConfigServerProperties(); + private EncryptionController controller = new EncryptionController( + new SingleTextEncryptorLocator(Encryptors.noOpText()), this.properties); + + @Test(expected = KeyNotInstalledException.class) + public void cannotDecryptWithoutKey() { + this.controller.decrypt("foo", MediaType.TEXT_PLAIN); + } + + @Test(expected = KeyNotInstalledException.class) + public void cannotDecryptWithNoopEncryptor() { + this.controller.decrypt("foo", MediaType.TEXT_PLAIN); + } + + @Test + public void sunnyDayRsaKey() { + this.controller = new EncryptionController(new SingleTextEncryptorLocator( + new RsaSecretEncryptor()), this.properties); + String cipher = this.controller.encrypt("foo", MediaType.TEXT_PLAIN); + assertEquals("foo", this.controller.decrypt(cipher, MediaType.TEXT_PLAIN)); + } + + @Test + public void publicKey() { + this.controller = new EncryptionController(new SingleTextEncryptorLocator( + new RsaSecretEncryptor()), this.properties); + String key = this.controller.getPublicKey(); + assertTrue("Wrong key format: " + key, key.startsWith("ssh-rsa")); + } + + @Test + public void appAndProfile() { + this.controller = new EncryptionController(new SingleTextEncryptorLocator( + new RsaSecretEncryptor()), this.properties); + // Add space to input + String cipher = this.controller.encrypt("app", "default", "foo bar", + MediaType.TEXT_PLAIN); + String decrypt = this.controller.decrypt("app", "default", cipher, + MediaType.TEXT_PLAIN); + assertEquals("Wrong decrypted plaintext: " + decrypt, "foo bar", decrypt); + } + + @Test + public void formDataIn() { + this.controller = new EncryptionController(new SingleTextEncryptorLocator( + new RsaSecretEncryptor()), this.properties); + // Add space to input + String cipher = this.controller.encrypt("foo bar=", + MediaType.APPLICATION_FORM_URLENCODED); + String decrypt = this.controller.decrypt(cipher + "=", + MediaType.APPLICATION_FORM_URLENCODED); + assertEquals("Wrong decrypted plaintext: " + decrypt, "foo bar", decrypt); + } + + @Test + public void formDataInWithPrefix() { + this.controller = new EncryptionController(new SingleTextEncryptorLocator( + new RsaSecretEncryptor()), this.properties); + // Add space to input + String cipher = this.controller.encrypt("{key:test}foo bar=", + MediaType.APPLICATION_FORM_URLENCODED); + String decrypt = this.controller.decrypt(cipher + "=", + MediaType.APPLICATION_FORM_URLENCODED); + assertEquals("Wrong decrypted plaintext: " + decrypt, "foo bar", decrypt); + } + + @Test + public void addEnvironment() { + TextEncryptorLocator locator = new TextEncryptorLocator() { + + private RsaSecretEncryptor encryptor = new RsaSecretEncryptor(); + + @Override + public TextEncryptor locate(Map keys) { + return this.encryptor; + } + }; + this.controller = new EncryptionController(locator, this.properties); + // Add space to input + String cipher = this.controller.encrypt("app", "default", "foo bar", + MediaType.TEXT_PLAIN); + assertFalse("Wrong cipher: " + cipher, cipher.contains("{name:app}")); + String decrypt = this.controller.decrypt("app", "default", cipher, + MediaType.TEXT_PLAIN); + assertEquals("Wrong decrypted plaintext: " + decrypt, "foo bar", decrypt); + } + +} diff --git a/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/encryption/EnvironmentPrefixHelperTests.java b/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/encryption/EnvironmentPrefixHelperTests.java new file mode 100644 index 00000000..c9b810aa --- /dev/null +++ b/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/encryption/EnvironmentPrefixHelperTests.java @@ -0,0 +1,84 @@ +/* + * Copyright 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 static org.junit.Assert.assertEquals; + +import java.util.Collections; +import java.util.Map; + +import org.junit.Test; + +/** + * @author Dave Syer + * + */ +public class EnvironmentPrefixHelperTests { + + private EnvironmentPrefixHelper helper = new EnvironmentPrefixHelper(); + + @Test + public void testAddPrefix() { + assertEquals("{bar:spam}foo", + this.helper.addPrefix(Collections.singletonMap("bar", "spam"), "foo")); + } + + @Test + public void testAddNoPrefix() { + assertEquals("foo", + this.helper.addPrefix(Collections. emptyMap(), "foo")); + } + + @Test + public void testStripNoPrefix() { + assertEquals("foo", this.helper.stripPrefix("foo")); + } + + @Test + public void testStripPrefix() { + assertEquals("foo", this.helper.stripPrefix("{key:foo}foo")); + } + + @Test + public void testStripPrefixWithEscape() { + assertEquals("{key:foo}foo", this.helper.stripPrefix("{plain}{key:foo}foo")); + } + + @Test + public void testKeysDefaults() { + Map keys = this.helper.getEncryptorKeys("foo", "bar", "spam"); + assertEquals("foo", keys.get("name")); + assertEquals("bar", keys.get("profiles")); + } + + @Test + public void testKeysWithPrefix() { + Map keys = this.helper.getEncryptorKeys("foo", "bar", + "{key:mykey}foo"); + assertEquals(3, keys.size()); + assertEquals("mykey", keys.get("key")); + } + + @Test + public void testKeysWithPrefixAndEscape() { + Map keys = this.helper.getEncryptorKeys("foo", "bar", + "{key:mykey}{plain}{foo:bar}foo"); + assertEquals(3, keys.size()); + assertEquals("mykey", keys.get("key")); + } + +} diff --git a/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/encryption/KeyStoreTextEncryptorLocatorTests.java b/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/encryption/KeyStoreTextEncryptorLocatorTests.java new file mode 100644 index 00000000..af06e411 --- /dev/null +++ b/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/encryption/KeyStoreTextEncryptorLocatorTests.java @@ -0,0 +1,72 @@ +/* + * Copyright 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 static org.junit.Assert.assertEquals; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +import org.junit.Test; +import org.springframework.core.io.ClassPathResource; +import org.springframework.security.crypto.encrypt.TextEncryptor; +import org.springframework.security.rsa.crypto.KeyStoreKeyFactory; + +/** + * @author Dave Syer + * + */ +public class KeyStoreTextEncryptorLocatorTests { + + private KeyStoreTextEncryptorLocator locator = new KeyStoreTextEncryptorLocator( + new KeyStoreKeyFactory(new ClassPathResource("server.jks"), + "letmein".toCharArray()), "changeme", "mytestkey"); + + @Test + public void testDefaults() { + TextEncryptor encryptor = this.locator.locate(Collections + . emptyMap()); + assertEquals("foo", encryptor.decrypt(encryptor.encrypt("foo"))); + } + + @Test + public void testDifferentKeyDefaultSecret() { + this.locator.setSecretLocator(new SecretLocator() { + + @Override + public char[] locate(String secret) { + assertEquals("changeme", secret); + // The actual secret for "mykey" is the same as the keystore password + return "letmein".toCharArray(); + } + }); + TextEncryptor encryptor = this.locator.locate(Collections + . singletonMap("key", "mykey")); + assertEquals("foo", encryptor.decrypt(encryptor.encrypt("foo"))); + } + + @Test + public void testDifferentKeyAndSecret() { + Map map = new HashMap(); + map.put("key", "mytestkey"); + map.put("secret", "changeme"); + TextEncryptor encryptor = this.locator.locate(map); + assertEquals("foo", encryptor.decrypt(encryptor.encrypt("foo"))); + } + +} diff --git a/spring-cloud-config-server/src/test/resources/server.jks b/spring-cloud-config-server/src/test/resources/server.jks new file mode 100644 index 0000000000000000000000000000000000000000..37c3357ad428b29deb2e9df4f26f66eb561068ad GIT binary patch literal 4451 zcmchZS5y;fw#SnYN>(fEWzN5d+{>uh>a=U0y?1({-^$&_5EDOaJ< zdNli*ulual!?(M=kpnyr>M4^bxG2JV*YjS{m+G;9zN%xPetPq86KP>z+}|z2!IP~| z?LyDI-+gkp=LsVw-ki@(wH} ze&QptnZ${#cU3|0Iti}tk{WjV&|Nt+Zh_rg1x;v?n|IcoV#?(XOz%_#_Gb|#4;g!B zb+k!7v)BiaaK-)9o+op@;XQf6AJe|!M73UXpg%Jr`8hfq#swl@SWWCr!^YZek~DdH zHFL-Xx5^KF$qu9t#pqiz*rT3$Ok0q~$HW_iuq8ZB7S_zew~?(1SOuUHmq#oVk7m+8 zDckqHxSP7oVQ&5ElwH#XU%*U(PlGfk)@_-Td$Re_cF0bB@c2G#KCq%~_kylo`2mON zOY=(1E#t>t@8FJNLy_FbU1I)?iW^6{C*I<4GyKnnbmf5!$w%3Z%3Gw#(&4MgNw1e! z%m~N#un*JT!xr5;9vzkXdu3pC)bwqB-TL9f?AFu4TZ7K7QqdY2SRD=Gt$6Kn#PgDr z8FGlfN29KCd(n4|S(w^|Lv7`75&=85g?=IVcExs$$Eb;P0dm??#SMrVxH2sSrE7M4 z7JIBB!uol>JP7Ukm@J&AJ7vCE*Sgoe^+|+G6bzN9Nqohi7_0n^2fG0R~)I8h*zzgfNSio{Yo1aULNM(@@e{ObW< zc^N}ImsaPhQYO`&MzBGE-Z{;9U8AfO8^#~4Ot+oc(n31*rVj=xexnl^Hv3R8m9rG7 z$Sg_adatdM)>akTc4tYa}w zb8g)COXcarGqDRV83pi1m(Wb>$gB1~uh}AqRb96<{u=54O&2(lsik;3 zJ{#oumN%i7-R~l0_>qxA;w!q?V_l7Oh0lANSlu-S^H05Ra#mXrBhhacf5}rz0nZH$k)x?+-8LGpV;Q(k_N-Ez``~T)k)n^cWtkxP1YCw z+bmCidWOCaj8=*BCwnw7z1{KP)US7KCUvPJTF<1~*acPXQM)fv`&bSfx=LZP=ZxCx z&{{QxQ^p6?cQ1#DCUkLn3ENw@nGsjaKKsUZxK3NxjT@Dgr_dsKHDf^$Oyu@LCJ#Q3+16fbRCS*5;Cs#esAB03+Y3(BeqhGTG*rbn8)o>mNn)X$*Rw ztwqs!%`#T32W7zl_UgT?lpE}0+?5RPgF2C{O}e*MT3bB|ZV+JVvE6BB-YjJkiVl}6 z5(pqR6_J3;002aua2(NnI1Xf-MFawZKw#<q3`90c6Say`#5{J z|1*l59&GIH?1P|()BYJnL=U}-v13PJynHcU@ar&2galk1E+ZxemlQ{s|3gZ`rGCr* zULrUk$G;Zg@=6flfYbmu4hRL|fIvXKCH=$u>hw07&cRcnP)98CHu`|e0CU5^Wapjd z6f=F1!V!<)ItTf80y>(d$-$8PdJQ)A@Qqq*j~gQhsIVnLJglM3?S7tSMQ~!Ay#iym zz`H4r`Q*0S?SSzb`ZYF_i@V>`JykUn^~+;c%e6n{E9$c)YDZ8Uu@MSitWywJq>t=< zm#!P^(?K4A!qxe{uui3^oD7a++FHC}D(U7R5gch8&;L^E=WQWf88udAwe)USw>8y< zi2SK%(!GeHXous}bxof$#rNi6*;fqnRF5-KkFjeeJ6Q?mpCyw+u%Vn;f7p&j6o_9V z_?A3>bZ;WsgaZPa-B0>H4VJ$PA_4+{xi{c!@N1X9c#R6o1ZLpNN?S`Uoa@L-fs{!< zZve3h9{K!h5idLGvWP%{*|nOdk{^db9<}LxKV5nJGL)i0%O#JJd9CNlE;(b-a6>5;R(y5GraX}w*hoyM zshAZD(y0=RzbE=#foy;?3_*adpO|XBeBFAyHe2nW@hhpV@J=2S^NTN~8%EC0KDhb9 zuFb-?(by!IrY@QPulc+0{}-C*O_G%+UZM#R90$Jm4`}kW+Mmfr2H45z_{<7L+L6Ju zV;QM8Xc+k8iXA^&NiijUY)0q&a`da}$maT4=t;yTUGKYYxx7PRQn<>GjOSQ*-WkdD z5U&@>ZtF8R!IgrL13RWIgQ!1HW}Rjxw|A2{d|0Tsw89ff%cRH+{Q?Fycb{7+WFar; z`ji2lEAwYhppmJT<=RdcucX>M>PtDS;mszlQQ1Wm=p8-KXxhe3m(H}735h{$vL$GF z`U=fllC3e?gjyQLJGW3dOWII@IgB9gGW66cGZ5J*oLp4NxQ)$PS&??#7sJUO5Z)8r z@xchZhu+$~I`GcWY$}zc9v)@WMV`~gqCohVBqTo}^RjO6pvPZ|tVznioGG)YY-3F# z;T0cag0`<$JItV~-(!Nk;s-K-JaVjDnMk~uD|nNJhH@;Y%3EWebVn~gn=e+@I6!0R zWWCQ;0grW!38kci*lDGd_I9T2Kn{HA*2>BMwmYZ!+*>-RJNDRXTV z^I2J=0&;pU)I?BHNloaW#p?}JInaYat?QEW*g)!ker#OXAjY z0nLbTo*!0))FqKkHi{da?{u&4^x2TDPE}62WH7S>-;~_xXz~j~PtnykXNZ92QQA;! zkv*sN98)gH)CSi&_vFohmq$U|wfSQAL|XNEQKou-E%NkUSYf-Up?5H0LuA7#y`t(7 zwgo+?pk&!^FjAWPI7j-^fndv)NPt7K=E?94FO>N82^XoCl2EHnfKkaOAM1MRQQFT6 zeaLZ0WQTahB;K#5FR02`_$-Z?l%guH9VP15v-ITU>cZjOE^67A+O=AIM-Q=IqQLFC zE==P=EQm_G0;Clv5(PF34Q$jVO0NAu1{Q@_kMVpm8*{6`= zB>ysQWaBgBbK;W?Y4z8<<kcsBeox%dh|ERpirOv!)xtNr z;;NELQWC>A1wL#h-EQ#O0&n5E{m6BJ7te`I7*&PZTAOmOwJ(m(f^>UpA!WA_{eBtS z(qCO=pP#luRjv1t@CV`%ihDlddx6|-4Y9ZL6vzyB3&%|q&NY8Xjsuyeof%+jpxf4# zCZ-EI0SAEbkx214E@46)F#YXLY{2l`l&~$LwG$Q1^{NlDH;8iNpY*V0wHEuoWDpd=8*r*A^KnE zH=ansWe^C2goMOjcp?Lr`Yr#<+5CTuQVzqs!djbOT(gl0R8Ij%O19n$$mkjnp72k$ za7`CwHSot2s2Q_l7If+!c8utaO5Rf2DT+k5erKVT4H--(ab}}NFr0BrRK5HXFV^_v zz3Kx9sG-#S;cLVvI!F0Ni+dj^Non)@I%&^&dnMiQF{JuM&%#>$$gS-!CacLT<7!c- zUr{tQw8D^`>8=4tZl0MN#m}1n8@EQH=DcjEYJ;~ z#}i9FVbYMjOs5aGrBB?UB>J|G^!^j0O!@{;FC?OOi+R~v76+p~;#zk;{98}`gHf;; zx9g9Og}h2ZY*$K4b<%0Xh9(2mS%j^J+1DP52&6Djfc-+E)gLf#pmM}aM1!>%9>ldi zb=DUj>LK`_@A+rmlJB*KU3}`My(Kx7DmZJ~L2Z5=e?~7*(Iz*{-eZ5$oI7z&EPm5* z$gd*&Vqrh0j{aQp$lgc~HyW<%f2!S)>BsrOse{_3V-qUQW=~2dk_x#Mp`-N2vc?wb zN76VS{&vfFmqb3z)e;9Syg|_Yv1u|S@pH3%BQl$Lrbk&ckujFSv1(hbBy|C9Y|ha& z!!%|ql+OI~Rp!O^!nza8at%#!Z%K6&BM42%(v8!bgopYUpjA~h^zz!s1i5zmL~5Wj U-Lo|r!PUhZftXVv!cy1&0IPeSMF0Q* literal 0 HcmV?d00001