diff --git a/spring-platform-config-server/pom.xml b/spring-platform-config-server/pom.xml index e4031d5b..2c4dc5ec 100644 --- a/spring-platform-config-server/pom.xml +++ b/spring-platform-config-server/pom.xml @@ -45,6 +45,15 @@ org.springframework.boot spring-boot-starter-web + + org.springframework.security + spring-security-crypto + + + org.springframework.security + spring-security-rsa + 1.0.0.BUILD-SNAPSHOT + org.eclipse.jgit org.eclipse.jgit diff --git a/spring-platform-config-server/src/main/java/org/springframework/platform/config/server/Application.java b/spring-platform-config-server/src/main/java/org/springframework/platform/config/server/Application.java index 953fa11c..8f0ba7f5 100644 --- a/spring-platform-config-server/src/main/java/org/springframework/platform/config/server/Application.java +++ b/spring-platform-config-server/src/main/java/org/springframework/platform/config/server/Application.java @@ -1,6 +1,8 @@ package org.springframework.platform.config.server; +import javax.annotation.PostConstruct; + import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; @@ -10,34 +12,40 @@ import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Profile; import org.springframework.core.env.ConfigurableEnvironment; -import org.springframework.platform.config.Environment; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; @Configuration @ComponentScan @EnableAutoConfiguration -@RestController public class Application { - @Autowired - private EnvironmentRepository repository; - - @RequestMapping("/{name}/{env}") - public Environment master(@PathVariable String name, @PathVariable String env) { - return properties(name, env, "master"); - } - - @RequestMapping("/{name}/{env}/{label}") - public Environment properties(@PathVariable String name, @PathVariable String env, @PathVariable String label) { - return repository.findOne(name, env, label); - } - public static void main(String[] args) { SpringApplication.run(Application.class, args); } + @Configuration + @ConfigurationProperties("encrypt") + protected static class KeyConfiguration { + @Autowired + private EncryptionController controller; + + private String key; + + public String getKey() { + return key; + } + + public void setKey(String key) { + this.key = key; + } + + @PostConstruct + public void init() { + if (key!=null) { + controller.uploadKey(key); + } + } + } + @Configuration @Profile("native") protected static class NativeRepositoryConfiguration { diff --git a/spring-platform-config-server/src/main/java/org/springframework/platform/config/server/EncryptionController.java b/spring-platform-config-server/src/main/java/org/springframework/platform/config/server/EncryptionController.java new file mode 100644 index 00000000..10e0ecad --- /dev/null +++ b/spring-platform-config-server/src/main/java/org/springframework/platform/config/server/EncryptionController.java @@ -0,0 +1,206 @@ +/* + * 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.platform.config.server; + +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.Map; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.platform.config.Environment; +import org.springframework.platform.config.PropertySource; +import org.springframework.security.crypto.encrypt.Encryptors; +import org.springframework.security.crypto.encrypt.TextEncryptor; +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.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.ResponseBody; +import org.springframework.web.bind.annotation.RestController; + +/** + * @author Dave Syer + * + */ +@RestController +public class EncryptionController { + + private static Log logger = LogFactory.getLog(EncryptionController.class); + + // TODO: expose as config property + private static final String SALT = "deadbeef"; + + private TextEncryptor encryptor; + + public void setEncryptor(TextEncryptor encryptor) { + this.encryptor = encryptor; + } + + @RequestMapping(value = "/key", method = RequestMethod.GET) + public String getPublicKey() { + if (!(encryptor instanceof RsaKeyHolder)) { + throw new KeyNotAvailableException(); + } + return ((RsaKeyHolder) encryptor).getPublicKey(); + } + + @RequestMapping(value = "/key", method = RequestMethod.PUT) + public ResponseEntity> uploadKey(@RequestBody String data) { + + Map body = new HashMap(); + body.put("status", "OK"); + + if (data.contains("RSA PRIVATE KEY")) { + + try { + encryptor = new RsaSecretEncryptor(data); + body.put("publicKey", ((RsaKeyHolder) encryptor).getPublicKey()); + } + catch (IllegalArgumentException e) { + throw new KeyFormatException(); + } + + } + else if (data.startsWith("ssh-rsa") || data.contains("RSA PUBLIC KEY")) { + throw new KeyFormatException(); + } + else { + encryptor = Encryptors.text(data, SALT); + } + + return new ResponseEntity>(body, HttpStatus.CREATED); + + } + + @ExceptionHandler(KeyFormatException.class) + @ResponseBody + public ResponseEntity> keyFormat() { + Map body = new HashMap(); + body.put("status", "BAD_REQUEST"); + body.put("description", "Key data not in PEM format"); + return new ResponseEntity>(body, HttpStatus.BAD_REQUEST); + } + + @ExceptionHandler(KeyNotAvailableException.class) + @ResponseBody + 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() { + if (encryptor == null) { + throw new KeyNotInstalledException(); + } + return Collections. singletonMap("status", "OK"); + } + + @RequestMapping(value = "encrypt", method = RequestMethod.POST) + public String encrypt(@RequestBody String data) { + if (encryptor == null) { + throw new KeyNotInstalledException(); + } + return encryptor.encrypt(data); + } + + @RequestMapping(value = "decrypt", method = RequestMethod.POST) + public String decrypt(@RequestBody String data) { + if (encryptor == null) { + throw new KeyNotInstalledException(); + } + try { + return encryptor.decrypt(data); + } catch (IllegalArgumentException e) { + throw new InvalidCipherException(); + } + } + + @ExceptionHandler(KeyNotInstalledException.class) + @ResponseBody + public ResponseEntity> notInstalled() { + Map body = new HashMap(); + body.put("status", "NO_KEY"); + body.put("description", "No key was installed for encryption service"); + return new ResponseEntity>(body, HttpStatus.NOT_FOUND); + } + + @ExceptionHandler(InvalidCipherException.class) + @ResponseBody + public ResponseEntity> invlidCipher() { + Map body = new HashMap(); + body.put("status", "INVALID"); + body.put("description", "Text not encrypted with this key"); + return new ResponseEntity>(body, HttpStatus.BAD_REQUEST); + } + + public Environment decrypt(Environment environment) { + Environment result = new Environment(environment.getName(), + environment.getLabel()); + for (PropertySource source : environment.getPropertySources()) { + LinkedHashMap map = new LinkedHashMap( + source.getSource()); + for (Object key : map.keySet()) { + String name = key.toString(); + if (name.endsWith(".secret")) { + Object value = map.get(key); + map.remove(key); + name = name.substring(0, name.length() - ".secret".length()); + if (encryptor == null) { + map.put(name, value); + } + else { + try { + value = value == null ? null + : encryptor.decrypt(value.toString()); + } catch (Exception e) { + value = ""; + name = "invalid." + name; + logger.warn("Cannot decode key: " + key + " (" + e.getClass() + ": " + e.getMessage() + ")"); + } + map.put(name,value); + } + } + } + result.add(new PropertySource(source.getName(), map)); + } + return result; + } +} + +@SuppressWarnings("serial") +class KeyNotInstalledException extends RuntimeException { +} + +@SuppressWarnings("serial") +class KeyNotAvailableException extends RuntimeException { +} + +@SuppressWarnings("serial") +class KeyFormatException extends RuntimeException { +} + +@SuppressWarnings("serial") +class InvalidCipherException extends RuntimeException { +} diff --git a/spring-platform-config-server/src/main/java/org/springframework/platform/config/server/EnvironmentController.java b/spring-platform-config-server/src/main/java/org/springframework/platform/config/server/EnvironmentController.java new file mode 100644 index 00000000..362d62d9 --- /dev/null +++ b/spring-platform-config-server/src/main/java/org/springframework/platform/config/server/EnvironmentController.java @@ -0,0 +1,35 @@ + +package org.springframework.platform.config.server; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.platform.config.Environment; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +public class EnvironmentController { + + private EnvironmentRepository repository; + + private EncryptionController encryption; + + @Autowired + public EnvironmentController(EnvironmentRepository repository, + EncryptionController encryption) { + super(); + this.repository = repository; + this.encryption = encryption; + } + + @RequestMapping("/{name}/{env}") + public Environment master(@PathVariable String name, @PathVariable String env) { + return properties(name, env, "master"); + } + + @RequestMapping("/{name}/{env}/{label}") + public Environment properties(@PathVariable String name, @PathVariable String env, @PathVariable String label) { + return encryption.decrypt(repository.findOne(name, env, label)); + } + +} diff --git a/spring-platform-config-server/src/test/java/org/springframework/platform/config/server/EncryptionControllerTests.java b/spring-platform-config-server/src/test/java/org/springframework/platform/config/server/EncryptionControllerTests.java new file mode 100644 index 00000000..6d8e3495 --- /dev/null +++ b/spring-platform-config-server/src/test/java/org/springframework/platform/config/server/EncryptionControllerTests.java @@ -0,0 +1,97 @@ +/* + * 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.platform.config.server; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertTrue; + +import java.util.Collections; + +import org.junit.Test; +import org.springframework.platform.config.Environment; +import org.springframework.platform.config.PropertySource; +import org.springframework.security.rsa.crypto.RsaSecretEncryptor; + +/** + * @author Dave Syer + * + */ +public class EncryptionControllerTests { + + private EncryptionController controller = new EncryptionController(); + + @Test(expected = KeyNotInstalledException.class) + public void cannotDecryptWithoutKey() { + controller.decrypt("foo"); + } + + @Test(expected = KeyFormatException.class) + public void cannotUploadPublicKey() { + controller.uploadKey("ssh-rsa ..."); + } + + @Test(expected = KeyFormatException.class) + public void cannotUploadPublicKeyPemFormat() { + controller.uploadKey("---- BEGIN RSA PUBLIC KEY ..."); + } + + @Test(expected = InvalidCipherException.class) + public void invalidCipher() { + controller.uploadKey("foo"); + controller.decrypt("foo"); + } + + @Test + public void sunnyDaySymmetricKey() { + controller.uploadKey("foo"); + String cipher = controller.encrypt("foo"); + assertEquals("foo", controller.decrypt(cipher)); + } + + @Test + public void sunnyDayRsaKey() { + controller.setEncryptor(new RsaSecretEncryptor()); + String cipher = controller.encrypt("foo"); + assertEquals("foo", controller.decrypt(cipher)); + } + + @Test + public void publicKey() { + controller.setEncryptor(new RsaSecretEncryptor()); + String key = controller.getPublicKey(); + assertTrue("Wrong key format: " + key, key.startsWith("ssh-rsa")); + } + + @Test + public void decryptEnvironment() { + controller.uploadKey("foo"); + String cipher = controller.encrypt("foo"); + Environment environment = new Environment("foo", "bar"); + environment.add(new PropertySource("spam", Collections + . singletonMap("my.secret", cipher))); + Environment result = controller.decrypt(environment); + assertEquals("foo", result.getPropertySources().get(0).getSource().get("my")); + } + + @Test + public void randomizedCipher() { + controller.uploadKey("foo"); + String cipher = controller.encrypt("foo"); + assertNotEquals(cipher, controller.encrypt("foo")); + } + +}