Add encryption features
This commit is contained in:
@@ -45,6 +45,15 @@
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-web</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.security</groupId>
|
||||
<artifactId>spring-security-crypto</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.security</groupId>
|
||||
<artifactId>spring-security-rsa</artifactId>
|
||||
<version>1.0.0.BUILD-SNAPSHOT</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.eclipse.jgit</groupId>
|
||||
<artifactId>org.eclipse.jgit</artifactId>
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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<Map<String, Object>> uploadKey(@RequestBody String data) {
|
||||
|
||||
Map<String, Object> body = new HashMap<String, Object>();
|
||||
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<Map<String, Object>>(body, HttpStatus.CREATED);
|
||||
|
||||
}
|
||||
|
||||
@ExceptionHandler(KeyFormatException.class)
|
||||
@ResponseBody
|
||||
public ResponseEntity<Map<String, Object>> keyFormat() {
|
||||
Map<String, Object> body = new HashMap<String, Object>();
|
||||
body.put("status", "BAD_REQUEST");
|
||||
body.put("description", "Key data not in PEM format");
|
||||
return new ResponseEntity<Map<String, Object>>(body, HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
|
||||
@ExceptionHandler(KeyNotAvailableException.class)
|
||||
@ResponseBody
|
||||
public ResponseEntity<Map<String, Object>> keyUnavailable() {
|
||||
Map<String, Object> body = new HashMap<String, Object>();
|
||||
body.put("status", "NOT_FOUND");
|
||||
body.put("description", "No public key available");
|
||||
return new ResponseEntity<Map<String, Object>>(body, HttpStatus.NOT_FOUND);
|
||||
}
|
||||
|
||||
@RequestMapping(value = "encrypt/status", method = RequestMethod.GET)
|
||||
public Map<String, Object> status() {
|
||||
if (encryptor == null) {
|
||||
throw new KeyNotInstalledException();
|
||||
}
|
||||
return Collections.<String, Object> 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<Map<String, Object>> notInstalled() {
|
||||
Map<String, Object> body = new HashMap<String, Object>();
|
||||
body.put("status", "NO_KEY");
|
||||
body.put("description", "No key was installed for encryption service");
|
||||
return new ResponseEntity<Map<String, Object>>(body, HttpStatus.NOT_FOUND);
|
||||
}
|
||||
|
||||
@ExceptionHandler(InvalidCipherException.class)
|
||||
@ResponseBody
|
||||
public ResponseEntity<Map<String, Object>> invlidCipher() {
|
||||
Map<String, Object> body = new HashMap<String, Object>();
|
||||
body.put("status", "INVALID");
|
||||
body.put("description", "Text not encrypted with this key");
|
||||
return new ResponseEntity<Map<String, Object>>(body, HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
|
||||
public Environment decrypt(Environment environment) {
|
||||
Environment result = new Environment(environment.getName(),
|
||||
environment.getLabel());
|
||||
for (PropertySource source : environment.getPropertySources()) {
|
||||
LinkedHashMap<Object, Object> map = new LinkedHashMap<Object, Object>(
|
||||
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 = "<n/a>";
|
||||
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 {
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
|
||||
.<Object, Object> 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"));
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user