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. --- .../java/com/yahoo/vespa/security/tool/CliUtils.java | 8 ++++++-- .../com/yahoo/vespa/security/tool/ConsoleInput.java | 12 ++++++++++++ .../main/java/com/yahoo/vespa/security/tool/Main.java | 13 ++++++++++--- .../com/yahoo/vespa/security/tool/ToolInvocation.java | 1 + .../yahoo/vespa/security/tool/crypto/DecryptTool.java | 11 ++++++++++- .../yahoo/vespa/security/tool/crypto/ResealTool.java | 10 +++++++++- .../com/yahoo/vespa/security/tool/crypto/ToolUtils.java | 17 +++++++++++++---- 7 files changed, 61 insertions(+), 11 deletions(-) create mode 100644 vespaclient-java/src/main/java/com/yahoo/vespa/security/tool/ConsoleInput.java (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 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); } } -- cgit v1.2.3