diff --git a/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/encryption/EncryptionController.java b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/encryption/EncryptionController.java index 944e30aa..212d37d6 100644 --- a/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/encryption/EncryptionController.java +++ b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/encryption/EncryptionController.java @@ -32,6 +32,7 @@ import org.springframework.http.ResponseEntity; import org.springframework.security.crypto.codec.Hex; import org.springframework.security.crypto.encrypt.TextEncryptor; import org.springframework.security.rsa.crypto.RsaKeyHolder; +import org.springframework.security.rsa.crypto.RsaSecretEncryptor; import org.springframework.util.Base64Utils; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.PathVariable; @@ -43,6 +44,7 @@ import org.springframework.web.bind.annotation.RestController; /** * @author Dave Syer + * @author Tim Ysewyn * */ @RestController @@ -51,7 +53,7 @@ public class EncryptionController { private static Log logger = LogFactory.getLog(EncryptionController.class); - volatile private TextEncryptorLocator encryptor; + volatile private TextEncryptorLocator encryptorLocator; private EnvironmentPrefixHelper helper = new EnvironmentPrefixHelper(); @@ -59,8 +61,8 @@ public class EncryptionController { private String defaultProfile = "default"; - public EncryptionController(TextEncryptorLocator encryptor) { - this.encryptor = encryptor; + public EncryptionController(TextEncryptorLocator encryptorLocator) { + this.encryptorLocator = encryptorLocator; } public void setDefaultApplicationName(String defaultApplicationName) { @@ -73,62 +75,42 @@ public class EncryptionController { @RequestMapping(value = "/key", method = RequestMethod.GET) public String getPublicKey() { - TextEncryptor encryptor = this.encryptor - .locate(this.helper.getEncryptorKeys("application", "default", "")); - if (!(encryptor instanceof RsaKeyHolder)) { - throw new KeyNotAvailableException(); - } - return ((RsaKeyHolder) encryptor).getPublicKey(); + return getPublicKey(defaultApplicationName, defaultProfile); } @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, "")); + TextEncryptor encryptor = getEncryptor(name, profiles, ""); if (!(encryptor instanceof RsaKeyHolder)) { throw new KeyNotAvailableException(); } return ((RsaKeyHolder) encryptor).getPublicKey(); } - @ExceptionHandler(KeyFormatException.class) - public ResponseEntity> keyFormat() { - Map body = new HashMap<>(); - body.put("status", "BAD_REQUEST"); - body.put("description", "Key data not in correct format (PEM or jks keystore)"); - return new ResponseEntity<>(body, HttpStatus.BAD_REQUEST); - } - - @ExceptionHandler(KeyNotAvailableException.class) - public ResponseEntity> keyUnavailable() { - Map body = new HashMap<>(); - body.put("status", "NOT_FOUND"); - body.put("description", "No public key available"); - return new ResponseEntity<>(body, HttpStatus.NOT_FOUND); - } - @RequestMapping(value = "encrypt/status", method = RequestMethod.GET) public Map status() { - checkEncryptorInstalled("application", "default"); + TextEncryptor encryptor = getEncryptor(defaultApplicationName, defaultProfile, + ""); + validateEncryptionWeakness(encryptor); return Collections.singletonMap("status", "OK"); } @RequestMapping(value = "encrypt", method = RequestMethod.POST) public String encrypt(@RequestBody String data, @RequestHeader("Content-Type") MediaType type) { - - return encrypt(this.defaultApplicationName, this.defaultProfile, data, type); + return encrypt(defaultApplicationName, defaultProfile, 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); + TextEncryptor encryptor = getEncryptor(name, profiles, ""); + validateEncryptionWeakness(encryptor); String input = stripFormData(data, type, false); - Map keys = this.helper.getEncryptorKeys(name, profiles, input); - String textToEncrypt = this.helper.stripPrefix(input); - String encrypted = this.helper.addPrefix(keys, - this.encryptor.locate(keys).encrypt(textToEncrypt)); + Map keys = helper.getEncryptorKeys(name, profiles, input); + String textToEncrypt = helper.stripPrefix(input); + String encrypted = helper.addPrefix(keys, + encryptorLocator.locate(keys).encrypt(textToEncrypt)); logger.info("Encrypted data"); return encrypted; } @@ -136,19 +118,18 @@ public class EncryptionController { @RequestMapping(value = "decrypt", method = RequestMethod.POST) public String decrypt(@RequestBody String data, @RequestHeader("Content-Type") MediaType type) { - - return decrypt(this.defaultApplicationName, this.defaultProfile, data, type); + return decrypt(defaultApplicationName, defaultProfile, 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); + TextEncryptor encryptor = getEncryptor(name, profiles, ""); + checkDecryptionPossible(encryptor); + validateEncryptionWeakness(encryptor); try { - String input = stripFormData(this.helper.stripPrefix(data), type, true); - Map encryptorKeys = this.helper.getEncryptorKeys(name, - profiles, data); - TextEncryptor encryptor = this.encryptor.locate(encryptorKeys); + encryptor = getEncryptor(name, profiles, data); + String input = stripFormData(helper.stripPrefix(data), type, true); String decrypted = encryptor.decrypt(input); logger.info("Decrypted cipher data"); return decrypted; @@ -159,16 +140,31 @@ public class EncryptionController { } } - private void checkEncryptorInstalled(String name, String profiles) { - if (this.encryptor == null) { + private TextEncryptor getEncryptor(String name, String profiles, String data) { + if (encryptorLocator == null) { throw new KeyNotInstalledException(); } - if (this.encryptor.locate(this.helper.getEncryptorKeys(name, profiles, "")) - .encrypt("FOO").equals("FOO")) { + TextEncryptor encryptor = encryptorLocator + .locate(helper.getEncryptorKeys(name, profiles, data)); + if (encryptor == null) { + throw new KeyNotInstalledException(); + } + return encryptor; + } + + private void validateEncryptionWeakness(TextEncryptor textEncryptor) { + if (textEncryptor.encrypt("FOO").equals("FOO")) { throw new EncryptionTooWeakException(); } } + private void checkDecryptionPossible(TextEncryptor textEncryptor) { + if (textEncryptor instanceof RsaSecretEncryptor + && !((RsaSecretEncryptor) textEncryptor).canDecrypt()) { + throw new DecryptionNotSupportedException(); + } + } + private String stripFormData(String data, MediaType type, boolean cipher) { if (data.endsWith("=") && !type.equals(MediaType.TEXT_PLAIN)) { @@ -209,6 +205,30 @@ public class EncryptionController { } + @ExceptionHandler(KeyFormatException.class) + public ResponseEntity> keyFormat() { + Map body = new HashMap<>(); + body.put("status", "BAD_REQUEST"); + body.put("description", "Key data not in correct format (PEM or jks keystore)"); + return new ResponseEntity<>(body, HttpStatus.BAD_REQUEST); + } + + @ExceptionHandler(KeyNotAvailableException.class) + public ResponseEntity> keyUnavailable() { + Map body = new HashMap<>(); + body.put("status", "NOT_FOUND"); + body.put("description", "No public key available"); + return new ResponseEntity<>(body, HttpStatus.NOT_FOUND); + } + + @ExceptionHandler(DecryptionNotSupportedException.class) + public ResponseEntity> decryptionDisabled() { + Map body = new HashMap<>(); + body.put("status", "BAD_REQUEST"); + body.put("description", "Server-side decryption is not supported"); + return new ResponseEntity<>(body, HttpStatus.BAD_REQUEST); + } + @ExceptionHandler(KeyNotInstalledException.class) public ResponseEntity> notInstalled() { Map body = new HashMap<>(); @@ -254,3 +274,8 @@ class EncryptionTooWeakException extends RuntimeException { class InvalidCipherException extends RuntimeException { } + +@SuppressWarnings("serial") +class DecryptionNotSupportedException extends RuntimeException { + +} diff --git a/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/encryption/EncryptionIntegrationTests.java b/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/encryption/EncryptionIntegrationTests.java index 5a92e043..37e3e3d4 100644 --- a/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/encryption/EncryptionIntegrationTests.java +++ b/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/encryption/EncryptionIntegrationTests.java @@ -16,6 +16,7 @@ package org.springframework.cloud.config.server.encryption; +import org.junit.BeforeClass; import org.junit.Test; import org.junit.runner.RunWith; @@ -23,7 +24,11 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.web.client.TestRestTemplate; import org.springframework.cloud.config.server.ConfigServerApplication; +import org.springframework.cloud.config.server.test.ConfigServerTestUtils; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.context.ActiveProfiles; @@ -93,4 +98,46 @@ public class EncryptionIntegrationTests { } + @RunWith(SpringRunner.class) + @SpringBootTest(classes = { ConfigServerApplication.class }, properties = { + "spring.cloud.bootstrap.name:keystore-bootstrap", + "spring.cloud.config.server.encrypt.enabled=false", + "encrypt.keyStore.alias=myencryptionkey" }, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) + @ActiveProfiles({ "test", "git" }) + @DirtiesContext + public static class KeystoreConfigurationEncryptionOnlyIntegrationTests { + + @Autowired + private TestRestTemplate testRestTemplate; + + @BeforeClass + public static void setupTest() throws Exception { + ConfigServerTestUtils.prepareLocalRepo("./", "target/repos", "encrypt-repo", + "target/config"); + } + + @Test + public void shouldOnlySupportEncryption() { + ResponseEntity entity = this.testRestTemplate + .getForEntity("/keystore-bootstrap/encrypt", String.class); + assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK); + assertThat(entity.getBody()).contains( + "{cipher}{key:mytestkey}AQCohs2V6P8/UiG6a4TF/CZTCBdt5Q7wvNvcyf6vs2ByK2ZYSM77Nu0sOAduxUpMbVwJ/syecmkIXR+hU3EfT2uqPieA7/v5n33ppqIQ9JAt5JggdYIGe+wX25zU3DTXOOJdAAMzNX+zjOVyCh0QtmJf/kFslg6NqQq0E+kSg3zBi3AnkKj5BLnLIxkjxzKA4mnDXpSm7ekLZZP2iQSYSW/82AC7UOLLzTqwInMI3tJLW1e9Ne+LDsjmSxA+nkK9zhidtXPwb/SPaNF74cJCEf9mgzzKYwJlwqChLzJt8UQ1jHwRc8B6FufmizUHSp27nxdtVB4HMqh3nNsMCy137Ces58T09ZS/y/cYNRxcFbp78MHFHUqAgbC0B/p5t6h4XbQ="); + + HttpEntity encryptionRequest = new HttpEntity<>("valueToBeEncrypted"); + entity = this.testRestTemplate.postForEntity("/encrypt", encryptionRequest, + String.class); + assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK); + + HttpHeaders decryptionRequestHeaders = new HttpHeaders(); + decryptionRequestHeaders.setContentType(MediaType.TEXT_PLAIN); + HttpEntity decryptionRequest = new HttpEntity<>(entity.getBody(), + decryptionRequestHeaders); + entity = this.testRestTemplate.postForEntity("/decrypt", decryptionRequest, + String.class); + assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST); + } + + } + } diff --git a/spring-cloud-config-server/src/test/resources/server.jks b/spring-cloud-config-server/src/test/resources/server.jks index 37c3357a..230d48c1 100644 Binary files a/spring-cloud-config-server/src/test/resources/server.jks and b/spring-cloud-config-server/src/test/resources/server.jks differ