Merge branch '2.1.x'

This commit is contained in:
Tim Ysewyn
2019-12-11 14:22:29 +00:00
3 changed files with 118 additions and 46 deletions

View File

@@ -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<Map<String, Object>> keyFormat() {
Map<String, Object> 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<Map<String, Object>> keyUnavailable() {
Map<String, Object> 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<String, Object> 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<String, String> 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<String, String> 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<String, String> 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<Map<String, Object>> keyFormat() {
Map<String, Object> 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<Map<String, Object>> keyUnavailable() {
Map<String, Object> 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<Map<String, Object>> decryptionDisabled() {
Map<String, Object> 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<Map<String, Object>> notInstalled() {
Map<String, Object> body = new HashMap<>();
@@ -254,3 +274,8 @@ class EncryptionTooWeakException extends RuntimeException {
class InvalidCipherException extends RuntimeException {
}
@SuppressWarnings("serial")
class DecryptionNotSupportedException extends RuntimeException {
}

View File

@@ -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<String> 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<String> 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<String> decryptionRequest = new HttpEntity<>(entity.getBody(),
decryptionRequestHeaders);
entity = this.testRestTemplate.postForEntity("/decrypt", decryptionRequest,
String.class);
assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST);
}
}
}