From 33dfe070b89624bb9ccd4f7ff4d8ba8a350b23b1 Mon Sep 17 00:00:00 2001 From: Dave Syer Date: Fri, 5 Sep 2014 11:48:17 +0100 Subject: [PATCH] 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). --- spring-cloud-cli/pom.xml | 9 +- ...tCommand.java => CloudCommandFactory.java} | 8 +- .../encrypt/BaseEncryptOptionHandler.java | 108 ++++++++++++++++++ .../cli/command/encrypt/DecryptCommand.java | 57 +++++++++ .../cli/command/encrypt/EncryptCommand.java | 75 ++++++++++++ .../cli/command/encrypt/EncryptorFactory.java | 84 ++++++++++++++ .../command/encrypt/KeyFormatException.java | 20 ++++ .../rsa/crypto/ExtendedKeyHelper.java | 28 +++++ ...gframework.boot.cli.command.CommandFactory | 2 +- 9 files changed, 382 insertions(+), 9 deletions(-) rename spring-cloud-cli/src/main/java/org/springframework/cloud/cli/command/{EncryptCommand.java => CloudCommandFactory.java} (75%) create mode 100644 spring-cloud-cli/src/main/java/org/springframework/cloud/cli/command/encrypt/BaseEncryptOptionHandler.java create mode 100644 spring-cloud-cli/src/main/java/org/springframework/cloud/cli/command/encrypt/DecryptCommand.java create mode 100644 spring-cloud-cli/src/main/java/org/springframework/cloud/cli/command/encrypt/EncryptCommand.java create mode 100644 spring-cloud-cli/src/main/java/org/springframework/cloud/cli/command/encrypt/EncryptorFactory.java create mode 100644 spring-cloud-cli/src/main/java/org/springframework/cloud/cli/command/encrypt/KeyFormatException.java create mode 100644 spring-cloud-cli/src/main/java/org/springframework/security/rsa/crypto/ExtendedKeyHelper.java diff --git a/spring-cloud-cli/pom.xml b/spring-cloud-cli/pom.xml index 4040b32..b0573ed 100644 --- a/spring-cloud-cli/pom.xml +++ b/spring-cloud-cli/pom.xml @@ -38,15 +38,14 @@ + + org.springframework.security + spring-security-rsa + org.springframework.boot spring-boot-cli ${spring-boot.version} - provided - - - org.springframework.cloud - spring-cloud-netflix-eureka-server provided diff --git a/spring-cloud-cli/src/main/java/org/springframework/cloud/cli/command/EncryptCommand.java b/spring-cloud-cli/src/main/java/org/springframework/cloud/cli/command/CloudCommandFactory.java similarity index 75% rename from spring-cloud-cli/src/main/java/org/springframework/cloud/cli/command/EncryptCommand.java rename to spring-cloud-cli/src/main/java/org/springframework/cloud/cli/command/CloudCommandFactory.java index c9a0d9e..a249715 100644 --- a/spring-cloud-cli/src/main/java/org/springframework/cloud/cli/command/EncryptCommand.java +++ b/spring-cloud-cli/src/main/java/org/springframework/cloud/cli/command/CloudCommandFactory.java @@ -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 getCommands() { - return Collections.emptySet(); + return Arrays.asList(new EncryptCommand(), new DecryptCommand()); } } diff --git a/spring-cloud-cli/src/main/java/org/springframework/cloud/cli/command/encrypt/BaseEncryptOptionHandler.java b/spring-cloud-cli/src/main/java/org/springframework/cloud/cli/command/encrypt/BaseEncryptOptionHandler.java new file mode 100644 index 0000000..9cb2822 --- /dev/null +++ b/spring-cloud-cli/src/main/java/org/springframework/cloud/cli/command/encrypt/BaseEncryptOptionHandler.java @@ -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 keyOption; + + private OptionSpec aliasOption; + + private OptionSpec 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); + } + } + +} \ No newline at end of file diff --git a/spring-cloud-cli/src/main/java/org/springframework/cloud/cli/command/encrypt/DecryptCommand.java b/spring-cloud-cli/src/main/java/org/springframework/cloud/cli/command/encrypt/DecryptCommand.java new file mode 100644 index 0000000..75e9ac4 --- /dev/null +++ b/spring-cloud-cli/src/main/java/org/springframework/cloud/cli/command/encrypt/DecryptCommand.java @@ -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] "; + } + + 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; + } + + } + +} diff --git a/spring-cloud-cli/src/main/java/org/springframework/cloud/cli/command/encrypt/EncryptCommand.java b/spring-cloud-cli/src/main/java/org/springframework/cloud/cli/command/encrypt/EncryptCommand.java new file mode 100644 index 0000000..ab5e423 --- /dev/null +++ b/spring-cloud-cli/src/main/java/org/springframework/cloud/cli/command/encrypt/EncryptCommand.java @@ -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] "; + } + + private static class EncryptOptionHandler extends BaseEncryptOptionHandler { + + private OptionSpec 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; + } + + } + +} diff --git a/spring-cloud-cli/src/main/java/org/springframework/cloud/cli/command/encrypt/EncryptorFactory.java b/spring-cloud-cli/src/main/java/org/springframework/cloud/cli/command/encrypt/EncryptorFactory.java new file mode 100644 index 0000000..5d417ba --- /dev/null +++ b/spring-cloud-cli/src/main/java/org/springframework/cloud/cli/command/encrypt/EncryptorFactory.java @@ -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; + } + +} diff --git a/spring-cloud-cli/src/main/java/org/springframework/cloud/cli/command/encrypt/KeyFormatException.java b/spring-cloud-cli/src/main/java/org/springframework/cloud/cli/command/encrypt/KeyFormatException.java new file mode 100644 index 0000000..a76e9d0 --- /dev/null +++ b/spring-cloud-cli/src/main/java/org/springframework/cloud/cli/command/encrypt/KeyFormatException.java @@ -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 { +} \ No newline at end of file diff --git a/spring-cloud-cli/src/main/java/org/springframework/security/rsa/crypto/ExtendedKeyHelper.java b/spring-cloud-cli/src/main/java/org/springframework/security/rsa/crypto/ExtendedKeyHelper.java new file mode 100644 index 0000000..053f8bf --- /dev/null +++ b/spring-cloud-cli/src/main/java/org/springframework/security/rsa/crypto/ExtendedKeyHelper.java @@ -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); + } +} diff --git a/spring-cloud-cli/src/main/resources/META-INF/services/org.springframework.boot.cli.command.CommandFactory b/spring-cloud-cli/src/main/resources/META-INF/services/org.springframework.boot.cli.command.CommandFactory index bc928ea..d7b5678 100644 --- a/spring-cloud-cli/src/main/resources/META-INF/services/org.springframework.boot.cli.command.CommandFactory +++ b/spring-cloud-cli/src/main/resources/META-INF/services/org.springframework.boot.cli.command.CommandFactory @@ -1 +1 @@ -org.springframework.cloud.cli.command.EncryptCommand +org.springframework.cloud.cli.command.CloudCommandFactory