aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--vespaclient-java/src/main/java/com/yahoo/vespa/security/tool/CliUtils.java27
-rw-r--r--vespaclient-java/src/main/java/com/yahoo/vespa/security/tool/Main.java9
-rw-r--r--vespaclient-java/src/main/java/com/yahoo/vespa/security/tool/ToolInvocation.java2
-rw-r--r--vespaclient-java/src/main/java/com/yahoo/vespa/security/tool/crypto/CipherUtils.java25
-rw-r--r--vespaclient-java/src/main/java/com/yahoo/vespa/security/tool/crypto/DecryptTool.java18
-rw-r--r--vespaclient-java/src/main/java/com/yahoo/vespa/security/tool/crypto/EncryptTool.java14
-rw-r--r--vespaclient-java/src/test/java/com/yahoo/vespa/security/tool/CryptoToolsTest.java73
-rw-r--r--vespaclient-java/src/test/resources/expected-decrypt-help-output.txt7
-rw-r--r--vespaclient-java/src/test/resources/expected-encrypt-help-output.txt3
9 files changed, 137 insertions, 41 deletions
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<String, String> 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(
"<encrypted file> <options>",
"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 {
"<input file> <options>",
"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) {
diff --git a/vespaclient-java/src/test/java/com/yahoo/vespa/security/tool/CryptoToolsTest.java b/vespaclient-java/src/test/java/com/yahoo/vespa/security/tool/CryptoToolsTest.java
index bb8024544cb..91ca30fd564 100644
--- a/vespaclient-java/src/test/java/com/yahoo/vespa/security/tool/CryptoToolsTest.java
+++ b/vespaclient-java/src/test/java/com/yahoo/vespa/security/tool/CryptoToolsTest.java
@@ -4,6 +4,7 @@ package com.yahoo.vespa.security.tool;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
+import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
@@ -15,6 +16,7 @@ import java.nio.file.attribute.PosixFilePermissions;
import java.util.List;
import java.util.Map;
+import static com.yahoo.security.ArrayUtils.toUtf8Bytes;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotEquals;
@@ -27,17 +29,19 @@ public class CryptoToolsTest {
private static record ProcessOutput(int exitCode, String stdOut, String stdErr) {}
+ private static final byte[] EMPTY_BYTES = new byte[0];
+
@TempDir
public File tmpFolder;
private void verifyStdoutMatchesFile(List<String> args, String expectedFile) throws IOException {
- var procOut = runMain(args, Map.of());
+ var procOut = runMain(args, EMPTY_BYTES, Map.of());
assertEquals(0, procOut.exitCode());
assertEquals(readTestResource(expectedFile), procOut.stdOut());
}
private void verifyStderrEquals(List<String> args, String expectedMessage) throws IOException {
- var procOut = runMain(args, Map.of());
+ var procOut = runMain(args, EMPTY_BYTES, Map.of());
assertEquals(1, procOut.exitCode()); // Assume checking stderr is because of a failure.
assertEquals(expectedMessage, procOut.stdErr());
}
@@ -161,7 +165,7 @@ public class CryptoToolsTest {
"--output-file", "foo",
"--recipient-public-key", TEST_PUB_KEY,
"--key-id", "1234"),
- "Invalid command line arguments: Cannot encrypt file 'no-such-file' as it does not exist\n");
+ "Invalid command line arguments: Input file 'no-such-file' does not exist\n");
}
@Test
@@ -188,7 +192,7 @@ public class CryptoToolsTest {
"--recipient-private-key-file", absPathOf(privKeyFile),
"--token", TEST_TOKEN,
"--key-id", Integer.toString(TEST_TOKEN_KEY_ID)),
- "Invalid command line arguments: Cannot decrypt file 'no-such-file' as it does not exist\n");
+ "Invalid command line arguments: Input file 'no-such-file' does not exist\n");
}
@Test
@@ -210,7 +214,7 @@ public class CryptoToolsTest {
}
@Test
- void can_end_to_end_keygen_encrypt_and_decrypt() throws IOException {
+ void can_end_to_end_keygen_encrypt_and_decrypt_via_files() throws IOException {
String greatSecret = "Dogs can't look up";
Path secretFile = pathInTemp("secret.txt");
@@ -261,19 +265,72 @@ public class CryptoToolsTest {
assertEquals(greatSecret, Files.readString(decryptedPath));
}
+ @Test
+ void can_end_to_end_keygen_encrypt_and_decrypt_via_stdio_streams() throws IOException {
+ String greatSecret = "forbidden knowledge about cats.txt";
+
+ var privPath = pathInTemp("priv.txt");
+ var pubPath = pathInTemp("pub.txt");
+ var procOut = runMain(List.of(
+ "keygen",
+ "--private-out-file", absPathOf(privPath),
+ "--public-out-file", absPathOf(pubPath)));
+ assertEquals(0, procOut.exitCode());
+ assertEquals("", procOut.stdOut());
+ assertEquals("", procOut.stdErr());
+
+ assertTrue(privPath.toFile().exists());
+ assertTrue(pubPath.toFile().exists());
+
+ var encryptedPath = pathInTemp("encrypted.bin");
+ // Encryption emits token on stdout, so can't support ciphertext output via that channel.
+ procOut = runMain(List.of(
+ "encrypt",
+ "-", // Encrypt stdin
+ "--output-file", absPathOf(encryptedPath),
+ "--recipient-public-key", Files.readString(pubPath),
+ "--key-id", "1234"),
+ toUtf8Bytes(greatSecret));
+ assertEquals(0, procOut.exitCode());
+ assertEquals("", procOut.stdErr());
+
+ var token = procOut.stdOut();
+ assertFalse(token.isBlank());
+
+ assertTrue(encryptedPath.toFile().exists());
+
+ procOut = runMain(List.of(
+ "decrypt",
+ "-", // Decrypt stdin
+ "--output-file", "-", // Plaintext to stdout
+ "--recipient-private-key-file", absPathOf(privPath),
+ "--key-id", "1234",
+ "--token", token
+ ), Files.readAllBytes(encryptedPath));
+
+ assertEquals(0, procOut.exitCode());
+ assertEquals("", procOut.stdErr());
+ assertEquals(greatSecret, procOut.stdOut());
+ }
+
private ProcessOutput runMain(List<String> args) {
+ return runMain(args, EMPTY_BYTES);
+ }
+
+ private ProcessOutput runMain(List<String> args, byte[] stdInBytes) {
// Expect that this is used for running a command that is not supposed to fail. But if it does,
// include exception trace in stderr to make it easier to debug.
- return runMain(args, Map.of("VESPA_DEBUG", "true"));
+ return runMain(args, stdInBytes, Map.of("VESPA_DEBUG", "true"));
}
- private ProcessOutput runMain(List<String> args, Map<String, String> env) {
+ private ProcessOutput runMain(List<String> args, byte[] stdInBytes, Map<String, String> env) {
var stdOutBytes = new ByteArrayOutputStream();
var stdErrBytes = new ByteArrayOutputStream();
+ var stdIn = new ByteArrayInputStream(stdInBytes);
var stdOut = new PrintStream(stdOutBytes);
var stdError = new PrintStream(stdErrBytes);
- int exitCode = new Main(stdOut, stdError).execute(args.toArray(new String[0]), env);
+ int exitCode = new Main(stdIn, stdOut, stdError).execute(args.toArray(new String[0]), env);
stdOut.flush();
stdError.flush();
diff --git a/vespaclient-java/src/test/resources/expected-decrypt-help-output.txt b/vespaclient-java/src/test/resources/expected-decrypt-help-output.txt
index 979133a562b..ef59741cd30 100644
--- a/vespaclient-java/src/test/resources/expected-decrypt-help-output.txt
+++ b/vespaclient-java/src/test/resources/expected-decrypt-help-output.txt
@@ -2,6 +2,9 @@ usage: vespa-security decrypt <encrypted file> <options>
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.
+
+To decrypt the contents of STDIN, specify an input file of '-' (without
+the quotes).
-h,--help Show help
-i,--key-id <arg> Numeric ID of recipient key. If
this is not provided, the key ID
@@ -9,7 +12,9 @@ given private key.
not verified.
-k,--recipient-private-key-file <arg> Recipient private key file
-o,--output-file <arg> Output file for decrypted
- plaintext
+ plaintext. Specify '-' (without
+ the quotes) to write plaintext to
+ STDOUT instead of a file.
-t,--token <arg> Token generated when the input
file was encrypted
Note: this is a BETA tool version; its interface may be changed at any
diff --git a/vespaclient-java/src/test/resources/expected-encrypt-help-output.txt b/vespaclient-java/src/test/resources/expected-encrypt-help-output.txt
index 848035f417e..5e1da32cbe7 100644
--- a/vespaclient-java/src/test/resources/expected-encrypt-help-output.txt
+++ b/vespaclient-java/src/test/resources/expected-encrypt-help-output.txt
@@ -3,6 +3,9 @@ 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.
+
+To encrypt the contents of STDIN, specify an input file of '-' (without
+the quotes).
-h,--help Show help
-i,--key-id <arg> Numeric ID of recipient key
-o,--output-file <arg> Output file (will be truncated if it