From 1f1a4d6ca6c0f3659fe43f6c050c0ae3bc4ea831 Mon Sep 17 00:00:00 2001 From: Tor Brede Vekterli Date: Fri, 18 Nov 2022 14:46:09 +0100 Subject: Support interactive private key entry when not using stdio redirection Avoids having to use a file indirection for inputting a private key. Only available when the JVM is running under an interactive console and none of the input/output files use standard streams. --- .../com/yahoo/vespa/security/tool/CliUtils.java | 8 +++++-- .../yahoo/vespa/security/tool/ConsoleInput.java | 12 ++++++++++ .../java/com/yahoo/vespa/security/tool/Main.java | 13 ++++++++--- .../yahoo/vespa/security/tool/ToolInvocation.java | 1 + .../vespa/security/tool/crypto/DecryptTool.java | 11 ++++++++- .../vespa/security/tool/crypto/ResealTool.java | 10 +++++++- .../vespa/security/tool/crypto/ToolUtils.java | 17 ++++++++++---- vespaclient-java/src/main/sh/vespa-crypto-cli.sh | 0 .../yahoo/vespa/security/tool/CryptoToolsTest.java | 27 +++++++++++++++++++--- .../resources/expected-decrypt-help-output.txt | 4 ++++ .../test/resources/expected-reseal-help-output.txt | 4 ++++ 11 files changed, 93 insertions(+), 14 deletions(-) create mode 100644 vespaclient-java/src/main/java/com/yahoo/vespa/security/tool/ConsoleInput.java mode change 100644 => 100755 vespaclient-java/src/main/sh/vespa-crypto-cli.sh (limited to 'vespaclient-java') 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 4eec1489360..a60c3647b41 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 @@ -22,8 +22,12 @@ public class CliUtils { return value; } + public static boolean useStdIo(String pathOrDash) { + return "-".equals(pathOrDash); + } + public static InputStream inputStreamFromFileOrStream(String pathOrDash, InputStream stdIn) throws IOException { - if ("-".equals(pathOrDash)) { + if (useStdIo(pathOrDash)) { return stdIn; } else { var inputPath = Paths.get(pathOrDash); @@ -35,7 +39,7 @@ public class CliUtils { } public static OutputStream outputStreamToFileOrStream(String pathOrDash, OutputStream stdOut) throws IOException { - if ("-".equals(pathOrDash)) { + if (useStdIo(pathOrDash)) { return stdOut; } else { // TODO fail if file already exists? diff --git a/vespaclient-java/src/main/java/com/yahoo/vespa/security/tool/ConsoleInput.java b/vespaclient-java/src/main/java/com/yahoo/vespa/security/tool/ConsoleInput.java new file mode 100644 index 00000000000..e77d5a51bc3 --- /dev/null +++ b/vespaclient-java/src/main/java/com/yahoo/vespa/security/tool/ConsoleInput.java @@ -0,0 +1,12 @@ +// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.security.tool; + +/** + * @author vekterli + */ +@FunctionalInterface +public interface ConsoleInput { + + String readPassword(String fmtPrompt, Object... fmtArgs); + +} 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 26868207bd3..6bbd6ae82a0 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 @@ -33,15 +33,22 @@ public class Main { private final InputStream stdIn; private final PrintStream stdOut; private final PrintStream stdError; + private final ConsoleInput consoleInputOrNull; - Main(InputStream stdIn, PrintStream stdOut, PrintStream stdError) { + Main(InputStream stdIn, PrintStream stdOut, PrintStream stdError, ConsoleInput consoleInputOrNull) { this.stdIn = stdIn; this.stdOut = stdOut; this.stdError = stdError; + this.consoleInputOrNull = consoleInputOrNull; + } + + private static ConsoleInput consoleOrNullFromJvm() { + var console = System.console(); + return console != null ? (prompt, args) -> new String(console.readPassword(prompt, args)).strip() : null; } public static void main(String[] args) { - var program = new Main(System.in, System.out, System.err); + var program = new Main(System.in, System.out, System.err, consoleOrNullFromJvm()); int returnCode = program.execute(args, System.getenv()); System.exit(returnCode); } @@ -89,7 +96,7 @@ public class Main { CliOptions.printToolSpecificHelp(stdOut, tool.name(), toolDesc, cliOpts); return 0; } - var invocation = new ToolInvocation(cmdLine, envVars, stdIn, stdOut, stdError, debugMode); + var invocation = new ToolInvocation(cmdLine, envVars, stdIn, stdOut, stdError, consoleInputOrNull, 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 13df714a268..ac4ed6fb8f7 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 @@ -16,6 +16,7 @@ public record ToolInvocation(CommandLine arguments, InputStream stdIn, PrintStream stdOut, PrintStream stdError, + ConsoleInput consoleInputOrNull, boolean debugMode) { public void printIfDebug(Supplier stringSupplier) { 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 b22afb7e5fb..ea79fe12c3d 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 @@ -13,6 +13,7 @@ import java.io.IOException; import java.util.List; import java.util.Optional; +import static com.yahoo.vespa.security.tool.crypto.ToolUtils.NO_INTERACTIVE_OPTION; import static com.yahoo.vespa.security.tool.crypto.ToolUtils.PRIVATE_KEY_DIR_OPTION; import static com.yahoo.vespa.security.tool.crypto.ToolUtils.PRIVATE_KEY_FILE_OPTION; @@ -51,6 +52,13 @@ public class DecryptTool implements Tool { .desc("Private key file directory used for automatically looking up " + "private keys based on the key ID specified as part of a token.") .build(), + Option.builder() + .longOpt(NO_INTERACTIVE_OPTION) + .hasArg(false) + .required(false) + .desc("Never ask for private key interactively if no private key file or " + + "directory is provided, even if process is running in a console") + .build(), Option.builder("e") .longOpt(EXPECTED_KEY_ID_OPTION) .hasArg(true) @@ -95,7 +103,8 @@ public class DecryptTool implements Tool { var sealedSharedKey = SealedSharedKey.fromTokenString(tokenString.strip()); ToolUtils.verifyExpectedKeyId(sealedSharedKey, maybeKeyId); - var privateKey = ToolUtils.resolvePrivateKeyFromInvocation(invocation, sealedSharedKey.keyId()); + var privateKey = ToolUtils.resolvePrivateKeyFromInvocation(invocation, sealedSharedKey.keyId(), + !CliUtils.useStdIo(inputArg) && !CliUtils.useStdIo(outputArg)); var secretShared = SharedKeyGenerator.fromSealedKey(sealedSharedKey, privateKey); var cipher = SharedKeyGenerator.makeAesGcmDecryptionCipher(secretShared); diff --git a/vespaclient-java/src/main/java/com/yahoo/vespa/security/tool/crypto/ResealTool.java b/vespaclient-java/src/main/java/com/yahoo/vespa/security/tool/crypto/ResealTool.java index 83fdf6998df..19be3e9fa51 100644 --- a/vespaclient-java/src/main/java/com/yahoo/vespa/security/tool/crypto/ResealTool.java +++ b/vespaclient-java/src/main/java/com/yahoo/vespa/security/tool/crypto/ResealTool.java @@ -15,6 +15,7 @@ import java.io.IOException; import java.util.List; import java.util.Optional; +import static com.yahoo.vespa.security.tool.crypto.ToolUtils.NO_INTERACTIVE_OPTION; import static com.yahoo.vespa.security.tool.crypto.ToolUtils.PRIVATE_KEY_DIR_OPTION; import static com.yahoo.vespa.security.tool.crypto.ToolUtils.PRIVATE_KEY_FILE_OPTION; @@ -45,6 +46,13 @@ public class ResealTool implements Tool { .desc("Private key file directory used for automatically looking up " + "private keys based on the key ID specified as part of a token.") .build(), + Option.builder() + .longOpt(NO_INTERACTIVE_OPTION) + .hasArg(false) + .required(false) + .desc("Never ask for private key interactively if no private key file or " + + "directory is provided, even if process is running in a console") + .build(), Option.builder("e") .longOpt(EXPECTED_KEY_ID_OPTION) .hasArg(true) @@ -97,7 +105,7 @@ public class ResealTool implements Tool { var recipientPubKey = KeyUtils.fromBase58EncodedX25519PublicKey(CliUtils.optionOrThrow(arguments, RECIPIENT_PUBLIC_KEY_OPTION).strip()); var recipientKeyId = KeyId.ofString(CliUtils.optionOrThrow(arguments, RECIPIENT_KEY_ID_OPTION)); - var privateKey = ToolUtils.resolvePrivateKeyFromInvocation(invocation, sealedSharedKey.keyId()); + var privateKey = ToolUtils.resolvePrivateKeyFromInvocation(invocation, sealedSharedKey.keyId(), true); var secretShared = SharedKeyGenerator.fromSealedKey(sealedSharedKey, privateKey); var resealedShared = SharedKeyGenerator.reseal(secretShared, recipientPubKey, recipientKeyId); diff --git a/vespaclient-java/src/main/java/com/yahoo/vespa/security/tool/crypto/ToolUtils.java b/vespaclient-java/src/main/java/com/yahoo/vespa/security/tool/crypto/ToolUtils.java index 2a25832708c..11e227f29b5 100644 --- a/vespaclient-java/src/main/java/com/yahoo/vespa/security/tool/crypto/ToolUtils.java +++ b/vespaclient-java/src/main/java/com/yahoo/vespa/security/tool/crypto/ToolUtils.java @@ -23,6 +23,7 @@ public class ToolUtils { static final String PRIVATE_KEY_FILE_OPTION = "private-key-file"; static final String PRIVATE_KEY_DIR_OPTION = "private-key-dir"; + static final String NO_INTERACTIVE_OPTION = "no-interactive"; static final String PRIVATE_KEY_DIR_ENV_VAR = "VESPA_CRYPTO_CLI_PRIVATE_KEY_DIR"; static final Pattern SAFE_KEY_ID_PATTERN = Pattern.compile("^[a-zA-Z0-9_-]+$"); @@ -41,8 +42,7 @@ public class ToolUtils { private static void verifyKeyIdIsPathSafe(KeyId keyId) { String keyIdStr = keyId.asString(); if (!SAFE_KEY_ID_PATTERN.matcher(keyIdStr).matches()) { - throw new IllegalArgumentException("The token key ID is not comprised of path-safe characters; refusing " + - "to auto-deduce key file name"); + throw new IllegalArgumentException("The token key ID is not comprised of path-safe characters; refusing to use it"); } } @@ -69,9 +69,10 @@ public class ToolUtils { return KeyUtils.fromBase58EncodedX25519PrivateKey(Files.readString(keyPath).strip()); } - public static XECPrivateKey resolvePrivateKeyFromInvocation(ToolInvocation invocation, KeyId tokenKeyId) throws IOException { + public static XECPrivateKey resolvePrivateKeyFromInvocation(ToolInvocation invocation, KeyId tokenKeyId, boolean mayReadKeyFromStdIn) throws IOException { var arguments = invocation.arguments(); var envVars = invocation.envVars(); + var console = invocation.consoleInputOrNull(); if (arguments.hasOption(PRIVATE_KEY_FILE_OPTION)) { if (arguments.hasOption(PRIVATE_KEY_DIR_OPTION)) { @@ -93,9 +94,17 @@ public class ToolUtils { : envVars.get(PRIVATE_KEY_DIR_ENV_VAR)); invocation.printIfDebug(() -> "Using private key lookup directory '%s'".formatted(privKeyDirPath)); return attemptResolvePrivateKeyFromDir(privKeyDirPath, tokenKeyId); - } else { + } else if (arguments.hasOption(NO_INTERACTIVE_OPTION) || (console == null) || !mayReadKeyFromStdIn) { throw new IllegalArgumentException("No private key specified. Must specify either --%s or --%s" .formatted(PRIVATE_KEY_FILE_OPTION, PRIVATE_KEY_DIR_OPTION)); + } else { + // We have a console attached to the JVM, ask for private key interactively + verifyKeyIdIsPathSafe(tokenKeyId); // Don't want to emit random stuff to the console + String key = console.readPassword("Private key for key id '%s' in Base-58 format: ", tokenKeyId.asString()); + if (key.length() == 0) { + throw new IllegalArgumentException("No private key provided; aborting"); + } + return KeyUtils.fromBase58EncodedX25519PrivateKey(key); } } diff --git a/vespaclient-java/src/main/sh/vespa-crypto-cli.sh b/vespaclient-java/src/main/sh/vespa-crypto-cli.sh old mode 100644 new mode 100755 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 d6dd65e1b2c..d7b8f1f09ae 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 @@ -510,6 +510,19 @@ public class CryptoToolsTest { assertEquals(0, procOut.exitCode()); assertEquals(plaintextData, procOut.stdOut()); + // Interactive private key reads are triggered when no other key option is used and stdio is left alone + Path decryptedFile = pathInTemp("decrypted.txt"); + procOut = runMain(List.of( + "decrypt", + absPathOf(encryptedFile), + "--output-file", absPathOf(decryptedFile), + "--token", tokenToAlice + ), (prompt, args) -> alicePrivKeyStr); + assertEquals("", procOut.stdErr()); + assertEquals(0, procOut.exitCode()); + assertEquals("", procOut.stdOut()); // we mock the console input, so nothing output here + assertEquals(plaintextData, Files.readString(decryptedFile)); + // Path-unsafe token key IDs are not automatically looked up, but failed. procOut = runMain(List.of( "decrypt", @@ -518,7 +531,7 @@ public class CryptoToolsTest { "--token", unsafeIdToken ), EMPTY_BYTES, env); assertEquals("Invalid command line arguments: The token key ID is not comprised " + - "of path-safe characters; refusing to auto-deduce key file name\n", + "of path-safe characters; refusing to use it\n", procOut.stdErr()); assertEquals(1, procOut.exitCode()); assertEquals("", procOut.stdOut()); @@ -529,17 +542,25 @@ public class CryptoToolsTest { } private ProcessOutput runMain(List args, byte[] stdInBytes) { - return runMain(args, stdInBytes, Map.of()); + return runMain(args, stdInBytes, Map.of(), null); + } + + private ProcessOutput runMain(List args, ConsoleInput consoleInput) { + return runMain(args, EMPTY_BYTES, Map.of(), consoleInput); } private ProcessOutput runMain(List args, byte[] stdInBytes, Map env) { + return runMain(args, stdInBytes, env, null); + } + + private ProcessOutput runMain(List args, byte[] stdInBytes, Map env, ConsoleInput consoleInput) { 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(stdIn, stdOut, stdError).execute(args.toArray(new String[0]), env); + int exitCode = new Main(stdIn, stdOut, stdError, consoleInput).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 2ccb93e9a33..f00db2bb6b9 100644 --- a/vespaclient-java/src/test/resources/expected-decrypt-help-output.txt +++ b/vespaclient-java/src/test/resources/expected-decrypt-help-output.txt @@ -12,6 +12,10 @@ the quotes). provided, the key ID is not verified. -h,--help Show help -k,--private-key-file Private key file in Base58 encoded format + --no-interactive Never ask for private key interactively if + no private key file or directory is + provided, even if process is running in a + console -o,--output-file Output file for decrypted plaintext. Specify '-' (without the quotes) to write plaintext to STDOUT instead of a file. diff --git a/vespaclient-java/src/test/resources/expected-reseal-help-output.txt b/vespaclient-java/src/test/resources/expected-reseal-help-output.txt index 7aad08b3f9c..cb82bd434b4 100644 --- a/vespaclient-java/src/test/resources/expected-reseal-help-output.txt +++ b/vespaclient-java/src/test/resources/expected-reseal-help-output.txt @@ -13,6 +13,10 @@ Prints new token to STDOUT. -i,--key-id ID of recipient key -k,--private-key-file Private key file in Base58 encoded format + --no-interactive Never ask for private key interactively + if no private key file or directory is + provided, even if process is running in + a console -r,--recipient-public-key Recipient X25519 public key in Base58 encoded format Note: this is a BETA tool version; its interface may be changed at any -- cgit v1.2.3