aboutsummaryrefslogtreecommitdiffstats
path: root/vespaclient-java/src/main/java/com/yahoo
diff options
context:
space:
mode:
authorTor Brede Vekterli <vekterli@yahooinc.com>2022-11-18 14:46:09 +0100
committerTor Brede Vekterli <vekterli@yahooinc.com>2022-11-18 15:10:28 +0100
commit1f1a4d6ca6c0f3659fe43f6c050c0ae3bc4ea831 (patch)
tree68cfc3a69ac77901c3698d271b33fc768e5843df /vespaclient-java/src/main/java/com/yahoo
parentd48eeaa38e06607f0225fa3f3ad54764d64539cb (diff)
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.
Diffstat (limited to 'vespaclient-java/src/main/java/com/yahoo')
-rw-r--r--vespaclient-java/src/main/java/com/yahoo/vespa/security/tool/CliUtils.java8
-rw-r--r--vespaclient-java/src/main/java/com/yahoo/vespa/security/tool/ConsoleInput.java12
-rw-r--r--vespaclient-java/src/main/java/com/yahoo/vespa/security/tool/Main.java13
-rw-r--r--vespaclient-java/src/main/java/com/yahoo/vespa/security/tool/ToolInvocation.java1
-rw-r--r--vespaclient-java/src/main/java/com/yahoo/vespa/security/tool/crypto/DecryptTool.java11
-rw-r--r--vespaclient-java/src/main/java/com/yahoo/vespa/security/tool/crypto/ResealTool.java10
-rw-r--r--vespaclient-java/src/main/java/com/yahoo/vespa/security/tool/crypto/ToolUtils.java17
7 files changed, 61 insertions, 11 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 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<String> 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);
}
}