From c1f6b93a94d8bb75707cb2643abf026eca45be7d Mon Sep 17 00:00:00 2001 From: Tor Brede Vekterli Date: Mon, 31 Oct 2022 16:33:17 +0100 Subject: Support standard IO streams for several encryption tool commands Useful for avoiding the need for intermediate files, such as when piping the output of decryption to a Zstd decompressor. Adds stdio support to: * Encryption input * Decryption input * Decryption output Specified by substituting the file name with a single `-` character. --- .../com/yahoo/vespa/security/tool/CliUtils.java | 27 ++++++++++++++++++++++ .../java/com/yahoo/vespa/security/tool/Main.java | 9 +++++--- .../yahoo/vespa/security/tool/ToolInvocation.java | 2 ++ .../vespa/security/tool/crypto/CipherUtils.java | 25 ++++++++------------ .../vespa/security/tool/crypto/DecryptTool.java | 18 ++++++++------- .../vespa/security/tool/crypto/EncryptTool.java | 14 ++++++----- 6 files changed, 63 insertions(+), 32 deletions(-) (limited to 'vespaclient-java/src/main/java/com/yahoo') diff --git a/vespaclient-java/src/main/java/com/yahoo/vespa/security/tool/CliUtils.java b/vespaclient-java/src/main/java/com/yahoo/vespa/security/tool/CliUtils.java index e9b348ab2a2..df199c00eda 100644 --- a/vespaclient-java/src/main/java/com/yahoo/vespa/security/tool/CliUtils.java +++ b/vespaclient-java/src/main/java/com/yahoo/vespa/security/tool/CliUtils.java @@ -3,6 +3,12 @@ package com.yahoo.vespa.security.tool; import org.apache.commons.cli.CommandLine; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.file.Files; +import java.nio.file.Paths; + /** * @author vekterli */ @@ -16,4 +22,25 @@ public class CliUtils { return value; } + public static InputStream inputStreamFromFileOrStream(String pathOrDash, InputStream stdIn) throws IOException { + if ("-".equals(pathOrDash)) { + return stdIn; + } else { + var inputPath = Paths.get(pathOrDash); + if (!inputPath.toFile().exists()) { + throw new IllegalArgumentException("Input file '%s' does not exist".formatted(inputPath.toString())); + } + return Files.newInputStream(inputPath); + } + } + + public static OutputStream outputStreamToFileOrStream(String pathOrDash, OutputStream stdOut) throws IOException { + if ("-".equals(pathOrDash)) { + return stdOut; + } else { + // TODO fail if file already exists? + return Files.newOutputStream(Paths.get(pathOrDash)); + } + } + } diff --git a/vespaclient-java/src/main/java/com/yahoo/vespa/security/tool/Main.java b/vespaclient-java/src/main/java/com/yahoo/vespa/security/tool/Main.java index 4216ffb6ed4..7ca98e4b9ba 100644 --- a/vespaclient-java/src/main/java/com/yahoo/vespa/security/tool/Main.java +++ b/vespaclient-java/src/main/java/com/yahoo/vespa/security/tool/Main.java @@ -10,6 +10,7 @@ import org.apache.commons.cli.DefaultParser; import org.apache.commons.cli.Options; import org.apache.commons.cli.ParseException; +import java.io.InputStream; import java.io.PrintStream; import java.util.List; import java.util.Map; @@ -26,16 +27,18 @@ import java.util.Optional; */ public class Main { + private final InputStream stdIn; private final PrintStream stdOut; private final PrintStream stdError; - Main(PrintStream stdOut, PrintStream stdError) { + Main(InputStream stdIn, PrintStream stdOut, PrintStream stdError) { + this.stdIn = stdIn; this.stdOut = stdOut; this.stdError = stdError; } public static void main(String[] args) { - var program = new Main(System.out, System.err); + var program = new Main(System.in, System.out, System.err); int returnCode = program.execute(args, System.getenv()); System.exit(returnCode); } @@ -82,7 +85,7 @@ public class Main { CliOptions.printToolSpecificHelp(stdOut, tool.name(), toolDesc, cliOpts); return 0; } - var invocation = new ToolInvocation(cmdLine, envVars, stdOut, stdError, debugMode); + var invocation = new ToolInvocation(cmdLine, envVars, stdIn, stdOut, stdError, debugMode); return tool.invoke(invocation); } catch (ParseException e) { return handleException("Failed to parse command line arguments: " + e.getMessage(), e, debugMode); diff --git a/vespaclient-java/src/main/java/com/yahoo/vespa/security/tool/ToolInvocation.java b/vespaclient-java/src/main/java/com/yahoo/vespa/security/tool/ToolInvocation.java index b7340ebb749..d1ff2687137 100644 --- a/vespaclient-java/src/main/java/com/yahoo/vespa/security/tool/ToolInvocation.java +++ b/vespaclient-java/src/main/java/com/yahoo/vespa/security/tool/ToolInvocation.java @@ -3,6 +3,7 @@ package com.yahoo.vespa.security.tool; import org.apache.commons.cli.CommandLine; +import java.io.InputStream; import java.io.PrintStream; import java.util.Map; @@ -11,6 +12,7 @@ import java.util.Map; */ public record ToolInvocation(CommandLine arguments, Map envVars, + InputStream stdIn, PrintStream stdOut, PrintStream stdError, boolean debugMode) { diff --git a/vespaclient-java/src/main/java/com/yahoo/vespa/security/tool/crypto/CipherUtils.java b/vespaclient-java/src/main/java/com/yahoo/vespa/security/tool/crypto/CipherUtils.java index e3954558026..051189c20b6 100644 --- a/vespaclient-java/src/main/java/com/yahoo/vespa/security/tool/crypto/CipherUtils.java +++ b/vespaclient-java/src/main/java/com/yahoo/vespa/security/tool/crypto/CipherUtils.java @@ -4,8 +4,8 @@ package com.yahoo.vespa.security.tool.crypto; import javax.crypto.Cipher; import javax.crypto.CipherOutputStream; import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; +import java.io.InputStream; +import java.io.OutputStream; /** * @author vekterli @@ -13,23 +13,18 @@ import java.nio.file.Path; public class CipherUtils { /** - * Streams the contents of fromPath into toPath after being wrapped by the input cipher. - * Depending on the Cipher mode, this either encrypts a plaintext file into ciphertext, - * or decrypts a ciphertext file into plaintext. + * Streams the contents of an input stream into an output stream after being wrapped by the input cipher. + * Depending on the Cipher mode, this either encrypts a plaintext stream into ciphertext, + * or decrypts a ciphertext stream into plaintext. * - * @param fromPath source file path to read from - * @param toPath destination file path to write to + * @param input source stream to read from + * @param output destination stream to write to * @param cipher a Cipher in either ENCRYPT or DECRYPT mode * @throws IOException if any file operation fails */ - public static void streamEncipherFileContents(Path fromPath, Path toPath, Cipher cipher) throws IOException { - if (fromPath.equals(toPath)) { - throw new IllegalArgumentException("Can't use same file as both input and output for enciphering"); - } - try (var inStream = Files.newInputStream(fromPath); - var outStream = Files.newOutputStream(toPath); - var cipherStream = new CipherOutputStream(outStream, cipher)) { - inStream.transferTo(cipherStream); + public static void streamEncipher(InputStream input, OutputStream output, Cipher cipher) throws IOException { + try (var cipherStream = new CipherOutputStream(output, cipher)) { + input.transferTo(cipherStream); cipherStream.flush(); } } diff --git a/vespaclient-java/src/main/java/com/yahoo/vespa/security/tool/crypto/DecryptTool.java b/vespaclient-java/src/main/java/com/yahoo/vespa/security/tool/crypto/DecryptTool.java index b307ab76da8..23543486e1b 100644 --- a/vespaclient-java/src/main/java/com/yahoo/vespa/security/tool/crypto/DecryptTool.java +++ b/vespaclient-java/src/main/java/com/yahoo/vespa/security/tool/crypto/DecryptTool.java @@ -36,7 +36,8 @@ public class DecryptTool implements Tool { .longOpt(OUTPUT_FILE_OPTION) .hasArg(true) .required(false) - .desc("Output file for decrypted plaintext") + .desc("Output file for decrypted plaintext. Specify '-' (without the " + + "quotes) to write plaintext to STDOUT instead of a file.") .build(), Option.builder("k") .longOpt(RECIPIENT_PRIVATE_KEY_FILE_OPTION) @@ -68,7 +69,8 @@ public class DecryptTool implements Tool { return new ToolDescription( " ", "Decrypts a file using a provided token and a secret private key. The file must " + - "previously have been encrypted using the public key component of the given private key.", + "previously have been encrypted using the public key component of the given private key.\n\n" + + "To decrypt the contents of STDIN, specify an input file of '-' (without the quotes).", "Note: this is a BETA tool version; its interface may be changed at any time", OPTIONS); } @@ -81,14 +83,11 @@ public class DecryptTool implements Tool { if (leftoverArgs.length != 1) { throw new IllegalArgumentException("Expected exactly 1 file argument to decrypt"); } - var inputPath = Paths.get(leftoverArgs[0]); - if (!inputPath.toFile().exists()) { - throw new IllegalArgumentException("Cannot decrypt file '%s' as it does not exist".formatted(inputPath.toString())); - } + var inputArg = leftoverArgs[0]; var maybeKeyId = Optional.ofNullable(arguments.hasOption(KEY_ID_OPTION) ? Integer.parseInt(arguments.getOptionValue(KEY_ID_OPTION)) : null); - var outputPath = Paths.get(CliUtils.optionOrThrow(arguments, OUTPUT_FILE_OPTION)); + var outputArg = CliUtils.optionOrThrow(arguments, OUTPUT_FILE_OPTION); var privKeyPath = Paths.get(CliUtils.optionOrThrow(arguments, RECIPIENT_PRIVATE_KEY_FILE_OPTION)); var tokenString = CliUtils.optionOrThrow(arguments, TOKEN_OPTION); var sealedSharedKey = SealedSharedKey.fromTokenString(tokenString.strip()); @@ -101,7 +100,10 @@ public class DecryptTool implements Tool { var secretShared = SharedKeyGenerator.fromSealedKey(sealedSharedKey, privateKey); var cipher = SharedKeyGenerator.makeAesGcmDecryptionCipher(secretShared); - CipherUtils.streamEncipherFileContents(inputPath, outputPath, cipher); + try (var inStream = CliUtils.inputStreamFromFileOrStream(inputArg, invocation.stdIn()); + var outStream = CliUtils.outputStreamToFileOrStream(outputArg, invocation.stdOut())) { + CipherUtils.streamEncipher(inStream, outStream, cipher); + } } catch (IOException e) { throw new RuntimeException(e); diff --git a/vespaclient-java/src/main/java/com/yahoo/vespa/security/tool/crypto/EncryptTool.java b/vespaclient-java/src/main/java/com/yahoo/vespa/security/tool/crypto/EncryptTool.java index 5d6cee2fabc..5437e8cf9fe 100644 --- a/vespaclient-java/src/main/java/com/yahoo/vespa/security/tool/crypto/EncryptTool.java +++ b/vespaclient-java/src/main/java/com/yahoo/vespa/security/tool/crypto/EncryptTool.java @@ -10,6 +10,7 @@ import com.yahoo.vespa.security.tool.ToolInvocation; import org.apache.commons.cli.Option; import java.io.IOException; +import java.nio.file.Files; import java.nio.file.Paths; import java.util.List; @@ -58,7 +59,8 @@ public class EncryptTool implements Tool { " ", "One-way encrypts a file using the public key of a recipient. A public token is printed on " + "standard out. The recipient can use this token to decrypt the file using their private key. " + - "The token does not have to be kept secret.", + "The token does not have to be kept secret.\n\n" + + "To encrypt the contents of STDIN, specify an input file of '-' (without the quotes).", "Note: this is a BETA tool version; its interface may be changed at any time", OPTIONS); } @@ -71,10 +73,7 @@ public class EncryptTool implements Tool { if (leftoverArgs.length != 1) { throw new IllegalArgumentException("Expected exactly 1 file argument to encrypt"); } - var inputPath = Paths.get(leftoverArgs[0]); - if (!inputPath.toFile().exists()) { - throw new IllegalArgumentException("Cannot encrypt file '%s' as it does not exist".formatted(inputPath.toString())); - } + var inputArg = leftoverArgs[0]; var outputPath = Paths.get(CliUtils.optionOrThrow(arguments, OUTPUT_FILE_OPTION)); var recipientPubKey = KeyUtils.fromBase64EncodedX25519PublicKey(CliUtils.optionOrThrow(arguments, RECIPIENT_PUBLIC_KEY_OPTION).strip()); @@ -82,7 +81,10 @@ public class EncryptTool implements Tool { var shared = SharedKeyGenerator.generateForReceiverPublicKey(recipientPubKey, keyId); var cipher = SharedKeyGenerator.makeAesGcmEncryptionCipher(shared); - CipherUtils.streamEncipherFileContents(inputPath, outputPath, cipher); + try (var inStream = CliUtils.inputStreamFromFileOrStream(inputArg, invocation.stdIn()); + var outStream = Files.newOutputStream(outputPath)) { + CipherUtils.streamEncipher(inStream, outStream, cipher); + } invocation.stdOut().println(shared.sealedSharedKey().toTokenString()); } catch (IOException e) { -- cgit v1.2.3