Add encrypt/decrypt commands (use -k to specify key)

The key format can be plaintext (symmetric key, not recommended but quick
and dirty), or PEM encoded key data, or (for encryption only) a base64
encoded public key (like in ~/.ssh/id_rsa or like you get from the
Spring Cloud configserver /key endpoint).
This commit is contained in:
Dave Syer
2014-09-05 11:48:17 +01:00
parent 89a5612f9f
commit 33dfe070b8
9 changed files with 382 additions and 9 deletions

View File

@@ -38,15 +38,14 @@
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-rsa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-cli</artifactId>
<version>${spring-boot.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-netflix-eureka-server</artifactId>
<scope>provided</scope>
</dependency>
<dependency>

View File

@@ -15,21 +15,23 @@
*/
package org.springframework.cloud.cli.command;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import org.springframework.boot.cli.command.Command;
import org.springframework.boot.cli.command.CommandFactory;
import org.springframework.cloud.cli.command.encrypt.DecryptCommand;
import org.springframework.cloud.cli.command.encrypt.EncryptCommand;
/**
* @author Dave Syer
*
*/
public class EncryptCommand implements CommandFactory {
public class CloudCommandFactory implements CommandFactory {
@Override
public Collection<Command> getCommands() {
return Collections.emptySet();
return Arrays.<Command>asList(new EncryptCommand(), new DecryptCommand());
}
}

View File

@@ -0,0 +1,108 @@
/*
* 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.cli.command.encrypt;
import static java.util.Arrays.asList;
import java.io.File;
import java.io.FileInputStream;
import java.nio.charset.Charset;
import joptsimple.OptionSet;
import joptsimple.OptionSpec;
import org.springframework.boot.cli.command.options.OptionHandler;
import org.springframework.boot.cli.util.Log;
import org.springframework.core.io.FileSystemResource;
import org.springframework.security.crypto.encrypt.TextEncryptor;
import org.springframework.security.rsa.crypto.KeyStoreKeyFactory;
import org.springframework.security.rsa.crypto.RsaSecretEncryptor;
import org.springframework.util.StreamUtils;
class BaseEncryptOptionHandler extends OptionHandler {
private OptionSpec<String> keyOption;
private OptionSpec<String> aliasOption;
private OptionSpec<String> passwordOption;
private Charset charset;
{
charset = Charset.forName("UTF-8");
}
@Override
protected final void options() {
this.keyOption = option(
asList("key", "k"),
"Specify key (symmetric secret, or pem-encoded key). If the value starts with @ it is interpreted as a file location.")
.withRequiredArg();
this.passwordOption = option("password",
"A password for the keyfile (assuming the --key option is a KetStore file).")
.withRequiredArg();
this.aliasOption = option("alias",
"An alias for the the key in a keyfile (assuming the --key option is a KetStore file).")
.withRequiredArg();
doOptions();
}
protected void doOptions() {
}
protected TextEncryptor createEncryptor(OptionSet options) {
String value = keyOption.value(options);
if (options.has(passwordOption)) { // it's a keystore
String password = options.valueOf(passwordOption);
String alias = options.valueOf(aliasOption);
KeyStoreKeyFactory factory = new KeyStoreKeyFactory(new FileSystemResource(
value), password.toCharArray());
RsaSecretEncryptor encryptor = new RsaSecretEncryptor(
factory.getKeyPair(alias));
return encryptor;
}
boolean verbose = Boolean.getBoolean("debug");
if (value.startsWith("@")) {
value = readFile(value.substring(1));
}
try {
value = readFile(value);
if (verbose) {
int len = Math.min(100, Math.max(value.length(), value.indexOf("\n")));
Log.info("File contents:\n" + value.substring(0, len) + "...");
}
}
catch (Exception e) {
// not a file
}
return new EncryptorFactory(verbose).create(value.trim());
}
private String readFile(String filename) {
try {
return StreamUtils.copyToString(new FileInputStream(new File(filename)),
charset);
}
catch (RuntimeException e) {
throw e;
}
catch (Exception e) {
throw new IllegalStateException(e);
}
}
}

View File

@@ -0,0 +1,57 @@
/*
* 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.cli.command.encrypt;
import joptsimple.OptionSet;
import org.springframework.boot.cli.command.OptionParsingCommand;
import org.springframework.boot.cli.command.status.ExitStatus;
import org.springframework.security.crypto.encrypt.TextEncryptor;
import org.springframework.util.StringUtils;
/**
* @author Dave Syer
*
*/
public class DecryptCommand extends OptionParsingCommand {
public DecryptCommand() {
super("decrypt", "Decrypt a string previsouly encrypted with the same key (or key pair)",
new DecryptOptionHandler());
}
@Override
public String getUsageHelp() {
return "[options] <text>";
}
private static class DecryptOptionHandler extends BaseEncryptOptionHandler {
@Override
protected synchronized ExitStatus run(OptionSet options) throws Exception {
TextEncryptor encryptor = createEncryptor(options);
String text = StringUtils.collectionToDelimitedString(
options.nonOptionArguments(), " ");
if (text.startsWith("{cipher}")) {
text = text.substring("{cipher}".length());
}
System.out.println(encryptor.decrypt(text));
return ExitStatus.OK;
}
}
}

View File

@@ -0,0 +1,75 @@
/*
* 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.cli.command.encrypt;
import static java.util.Arrays.asList;
import joptsimple.OptionSet;
import joptsimple.OptionSpec;
import org.springframework.boot.cli.command.OptionParsingCommand;
import org.springframework.boot.cli.command.status.ExitStatus;
import org.springframework.security.crypto.encrypt.TextEncryptor;
import org.springframework.util.StringUtils;
/**
* @author Dave Syer
*
*/
public class EncryptCommand extends OptionParsingCommand {
public EncryptCommand() {
super("encrypt", "Encrypt a string so, for isntance, it can be added to source control",
new EncryptOptionHandler());
}
@Override
public String getUsageHelp() {
return "[options] <text>";
}
private static class EncryptOptionHandler extends BaseEncryptOptionHandler {
private OptionSpec<String> propertyOption;
@Override
protected void doOptions() {
this.propertyOption = option(
asList("property", "p"),
"A name for the encrypted value. Output will be in a form that can be pasted in to a properties file.")
.withRequiredArg();
}
@Override
protected synchronized ExitStatus run(OptionSet options) throws Exception {
TextEncryptor encryptor = createEncryptor(options);
String text = StringUtils.collectionToDelimitedString(
options.nonOptionArguments(), " ");
System.out.println(formatCipher(options, encryptor.encrypt(text)));
return ExitStatus.OK;
}
protected String formatCipher(OptionSet options, String output) {
if (options.has(propertyOption)) {
output = options.valueOf(propertyOption).replace(":", "\\:")
.replace("=", "\\=").replace(" ", "\\ ")
+ "={cipher}" + output;
}
return output;
}
}
}

View File

@@ -0,0 +1,84 @@
/*
* 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.cli.command.encrypt;
import org.springframework.boot.cli.util.Log;
import org.springframework.security.crypto.encrypt.Encryptors;
import org.springframework.security.crypto.encrypt.TextEncryptor;
import org.springframework.security.rsa.crypto.ExtendedKeyHelper;
import org.springframework.security.rsa.crypto.RsaSecretEncryptor;
/**
* @author Dave Syer
*
*/
public class EncryptorFactory {
// TODO: expose as config property
private static final String SALT = "deadbeef";
private final boolean verbose;
public EncryptorFactory() {
this(false);
}
public EncryptorFactory(boolean verbose) {
this.verbose = verbose;
}
public TextEncryptor create(String data) {
TextEncryptor encryptor = null;
try {
encryptor = new RsaSecretEncryptor(data);
}
catch (IllegalArgumentException e) {
if (verbose) {
Log.info("Could not create RSA Encryptor (" + e.getMessage() + ")");
}
}
if (encryptor == null) {
if (verbose) {
Log.info("Trying public key");
}
try {
encryptor = new RsaSecretEncryptor(ExtendedKeyHelper.parsePublicKey(data));
}
catch (IllegalArgumentException e) {
if (verbose) {
Log.info("Could not create public key RSA Encryptor ("
+ e.getMessage() + ")");
}
}
}
if (encryptor == null) {
if (verbose) {
Log.info("Trying symmetric key");
}
encryptor = Encryptors.text(data, SALT);
}
if (encryptor == null) {
if (verbose) {
Log.error("Could not create any Encryptor");
}
throw new KeyFormatException();
}
return encryptor;
}
}

View File

@@ -0,0 +1,20 @@
/*
* 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.cli.command.encrypt;
@SuppressWarnings("serial")
public class KeyFormatException extends RuntimeException {
}

View File

@@ -0,0 +1,28 @@
/*
* 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.security.rsa.crypto;
import java.security.interfaces.RSAPublicKey;
/**
* @author Dave Syer
*
*/
public class ExtendedKeyHelper extends RsaKeyHelper {
public static RSAPublicKey parsePublicKey(String key) {
return RsaKeyHelper.parsePublicKey(key);
}
}

View File

@@ -1 +1 @@
org.springframework.cloud.cli.command.EncryptCommand
org.springframework.cloud.cli.command.CloudCommandFactory