summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorTor Brede Vekterli <vekterli@yahooinc.com>2023-01-30 14:41:01 +0100
committerTor Brede Vekterli <vekterli@yahooinc.com>2023-01-31 17:20:36 +0100
commit5ffdfd6d0bc77eda829054c9c3de6fba950507de (patch)
treeddbb173a6300fce2a7c3cf1ee70878d252f4a947
parent3e54969fc961ee51c93404a37d559ab7ea2f9fe6 (diff)
Add an "interactive" token resealing protocol and basic tooling support
Implements a protocol for delegated access to a shared secret key of a token whose private key we do not possess. This builds directly on top of the existing token resealing mechanisms. The primary benefit of the resealing protocol is that none of the data exchanged can reveal anything about the underlying secret. Security note: neither resealing requests nor responses are explicitly authenticated (this is a property inherited from the sealed shared key tokens themselves). It is assumed that an attacker can observe all requests and responses in transit, but cannot modify them.
-rw-r--r--security-utils/src/main/java/com/yahoo/security/SealedSharedKey.java19
-rw-r--r--security-utils/src/main/java/com/yahoo/security/SharedKeyResealingSession.java155
-rw-r--r--security-utils/src/main/java/com/yahoo/security/hpke/Hpke.java8
-rw-r--r--security-utils/src/test/java/com/yahoo/security/SharedKeyTest.java25
-rw-r--r--vespaclient-java/src/main/java/com/yahoo/vespa/security/tool/crypto/DecryptTool.java49
-rw-r--r--vespaclient-java/src/main/java/com/yahoo/vespa/security/tool/crypto/ResealTool.java55
-rw-r--r--vespaclient-java/src/test/resources/expected-decrypt-help-output.txt2
-rw-r--r--vespaclient-java/src/test/resources/expected-reseal-help-output.txt2
8 files changed, 286 insertions, 29 deletions
diff --git a/security-utils/src/main/java/com/yahoo/security/SealedSharedKey.java b/security-utils/src/main/java/com/yahoo/security/SealedSharedKey.java
index d570cd799cc..99d07465812 100644
--- a/security-utils/src/main/java/com/yahoo/security/SealedSharedKey.java
+++ b/security-utils/src/main/java/com/yahoo/security/SealedSharedKey.java
@@ -20,11 +20,14 @@ public record SealedSharedKey(int version, KeyId keyId, byte[] enc, byte[] ciphe
public static final int CURRENT_TOKEN_VERSION = 2;
/** Encryption context for v{1,2} tokens is always a 32-byte X25519 public key */
public static final int MAX_ENC_CONTEXT_LENGTH = 255;
+ // Expected max decoded size for v1 is 3 + 255 + 32 + 32 = 322. For simplicity, round this
+ // up to 512 to effectively not have to care about the overhead of any reasonably chosen encoding.
+ public static final int MAX_TOKEN_STRING_LENGTH = 512;
public SealedSharedKey {
if (enc.length > MAX_ENC_CONTEXT_LENGTH) {
throw new IllegalArgumentException("Encryption context is too large to be encoded (max is %d, got %d)"
- .formatted(MAX_ENC_CONTEXT_LENGTH, enc.length));
+ .formatted(MAX_ENC_CONTEXT_LENGTH, enc.length));
}
}
@@ -33,6 +36,10 @@ public record SealedSharedKey(int version, KeyId keyId, byte[] enc, byte[] ciphe
* reconstruct the SealedSharedKey instance when passed verbatim to fromTokenString().
*/
public String toTokenString() {
+ return Base62.codec().encode(toSerializedBytes());
+ }
+
+ byte[] toSerializedBytes() {
byte[] keyIdBytes = keyId.asBytes();
// u8 token version || u8 length(key id) || key id || u8 length(enc) || enc || ciphertext
ByteBuffer encoded = ByteBuffer.allocate(1 + 1 + keyIdBytes.length + 1 + enc.length + ciphertext.length);
@@ -46,7 +53,7 @@ public record SealedSharedKey(int version, KeyId keyId, byte[] enc, byte[] ciphe
byte[] encBytes = new byte[encoded.remaining()];
encoded.get(encBytes);
- return Base62.codec().encode(encBytes);
+ return encBytes;
}
/**
@@ -56,6 +63,10 @@ public record SealedSharedKey(int version, KeyId keyId, byte[] enc, byte[] ciphe
public static SealedSharedKey fromTokenString(String tokenString) {
verifyInputTokenStringNotTooLarge(tokenString);
byte[] rawTokenBytes = Base62.codec().decode(tokenString);
+ return fromSerializedBytes(rawTokenBytes);
+ }
+
+ static SealedSharedKey fromSerializedBytes(byte[] rawTokenBytes) {
if (rawTokenBytes.length < 1) {
throw new IllegalArgumentException("Decoded token too small to contain a version");
}
@@ -81,9 +92,7 @@ public record SealedSharedKey(int version, KeyId keyId, byte[] enc, byte[] ciphe
public int tokenVersion() { return version; }
private static void verifyInputTokenStringNotTooLarge(String tokenString) {
- // Expected max decoded size for v1 is 3 + 255 + 32 + 32 = 322. For simplicity, round this
- // up to 512 to effectively not have to care about the overhead of any reasonably chosen encoding.
- if (tokenString.length() > 512) {
+ if (tokenString.length() > MAX_TOKEN_STRING_LENGTH) {
throw new IllegalArgumentException("Token string is too long to possibly be a valid token");
}
}
diff --git a/security-utils/src/main/java/com/yahoo/security/SharedKeyResealingSession.java b/security-utils/src/main/java/com/yahoo/security/SharedKeyResealingSession.java
new file mode 100644
index 00000000000..6e79b86d832
--- /dev/null
+++ b/security-utils/src/main/java/com/yahoo/security/SharedKeyResealingSession.java
@@ -0,0 +1,155 @@
+// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.security;
+
+import org.bouncycastle.util.Arrays;
+
+import java.nio.ByteBuffer;
+import java.security.KeyPair;
+import java.security.PrivateKey;
+import java.security.interfaces.XECPublicKey;
+import java.util.Optional;
+
+/**
+ * <p>Delegated resealing protocol for getting access to a shared secret key of a token
+ * whose private key we do not possess.</p>
+ *
+ * <p>The primary benefit of the interactive resealing protocol is that none of the data
+ * exchanged can reveal anything about the underlying sealed secret itself.</p>
+ *
+ * <p>Note that neither resealing requests nor responses are authenticated (this is a property
+ * inherited from the sealed shared key tokens themselves). It is assumed that an attacker
+ * can <em>observe</em> all requests and responses in transit, but cannot modify them.</p>
+ *
+ * <h2>Protocol details</h2>
+ *
+ * <p>Decryptor (requester):</p>
+ * <ol>
+ * <li>Create a resealing session instance that maintains an ephemeral X25519 key pair that
+ * is valid only for the lifetime of the session.</li>
+ * <li>Create a resealing request for a token <em>T</em>. The session emits a Base62-encoded
+ * binary representation of the tuple <em>&lt;ephemeral public key, T&gt;</em>.</li>
+ * <li>Send the request to the private key holder. The session must be kept alive until the
+ * response is received, or the ephemeral private key associated with the public key will
+ * be irrevocably lost.</li>
+ * </ol>
+ * <p>Private key holder (re-sealer):</p>
+ * <ol>
+ * <li>Decode Base62-encoded request into tuple <em>&lt;ephemeral public key, T&gt;</em>.</li>
+ * <li>Look up the correct private key from the key ID contained in token <em>T</em>.</li>
+ * <li>Reseal token <em>T</em> for the requested ephemeral public key using the correct private key.</li>
+ * <li>Return resealed token <em>T<sub>R</sub></em> to requester.</li>
+ * </ol>
+ * <p>Decryptor (requester):</p>
+ * <ol>
+ * <li>Decrypt token <em>T<sub>R</sub></em> using ephemeral private key.</li>
+ * <li>Use secret key in token to decrypt the payload protected by original token <em>T</em>.</li>
+ * </ol>
+ *
+ * @author vekterli
+ */
+public class SharedKeyResealingSession {
+
+ private final KeyPair ephemeralKeyPair;
+
+ SharedKeyResealingSession(KeyPair ephemeralKeyPair) {
+ this.ephemeralKeyPair = ephemeralKeyPair;
+ }
+
+ public static SharedKeyResealingSession newEphemeralSession() {
+ return new SharedKeyResealingSession(KeyUtils.generateX25519KeyPair());
+ }
+
+ @FunctionalInterface
+ public interface PrivateKeyProvider {
+ Optional<PrivateKey> privateKeyForId(KeyId id);
+ }
+
+ public record ResealingRequest(XECPublicKey ephemeralPubKey, SealedSharedKey sealedKey) {
+
+ private static final byte[] HEADER_BYTES = new byte[] {'R','S'};
+ private static final byte CURRENT_VERSION = 1;
+
+ public String toSerializedString() {
+ byte[] pubKeyBytes = KeyUtils.toRawX25519PublicKeyBytes(ephemeralPubKey);
+ byte[] tokenBytes = sealedKey.toSerializedBytes();
+
+ ByteBuffer encoded = ByteBuffer.allocate(HEADER_BYTES.length + 1 + 1 + pubKeyBytes.length + tokenBytes.length);
+ encoded.put(HEADER_BYTES);
+ encoded.put(CURRENT_VERSION);
+ encoded.put((byte)pubKeyBytes.length);
+ encoded.put(pubKeyBytes);
+ encoded.put(tokenBytes);
+ encoded.flip();
+
+ byte[] encBytes = new byte[encoded.remaining()];
+ encoded.get(encBytes);
+ return Base62.codec().encode(encBytes);
+ }
+
+ public static ResealingRequest fromSerializedString(String request) {
+ verifyInputStringNotTooLarge(request);
+ byte[] rawBytes = Base62.codec().decode(request);
+ if (rawBytes.length < HEADER_BYTES.length + 2) {
+ throw new IllegalArgumentException("Resealing request too short to contain a header and key length");
+ }
+ ByteBuffer decoded = ByteBuffer.wrap(rawBytes);
+ byte[] header = new byte[2];
+ decoded.get(header);
+ if (!Arrays.areEqual(header, HEADER_BYTES)) {
+ throw new IllegalArgumentException("No resealing request header found");
+ }
+ byte version = decoded.get();
+ if (version != CURRENT_VERSION) {
+ throw new IllegalArgumentException("Unsupported version in resealing request header");
+ }
+ int pubKeyLen = Byte.toUnsignedInt(decoded.get());
+ byte[] pubKeyBytes = new byte[pubKeyLen];
+ decoded.get(pubKeyBytes);
+
+ byte[] rawTokenBytes = new byte[decoded.remaining()];
+ decoded.get(rawTokenBytes);
+
+ return new ResealingRequest(KeyUtils.fromRawX25519PublicKey(pubKeyBytes),
+ SealedSharedKey.fromSerializedBytes(rawTokenBytes));
+ }
+
+ private static void verifyInputStringNotTooLarge(String tokenString) {
+ if (tokenString.length() > SealedSharedKey.MAX_TOKEN_STRING_LENGTH + 64) {
+ throw new IllegalArgumentException("String is too long to possibly be a valid resealing request");
+ }
+ }
+
+ }
+
+ public record ResealingResponse(SealedSharedKey resealedKey) {
+
+ public String toSerializedString() {
+ return resealedKey.toTokenString();
+ }
+
+ public static ResealingResponse fromSerializedString(String response) {
+ return new ResealingResponse(SealedSharedKey.fromTokenString(response));
+ }
+
+ }
+
+ public ResealingRequest resealingRequestFor(SealedSharedKey sealedSharedKey) {
+ return new ResealingRequest((XECPublicKey) ephemeralKeyPair.getPublic(), sealedSharedKey);
+ }
+
+ public static ResealingResponse reseal(ResealingRequest request, PrivateKeyProvider privateKeyProvider) {
+ var privKey = privateKeyProvider.privateKeyForId(request.sealedKey.keyId()).orElseThrow(
+ () -> new IllegalArgumentException("Could not find a private key for key ID '%s'".formatted(request.sealedKey.keyId())));
+
+ var secretShared = SharedKeyGenerator.fromSealedKey(request.sealedKey, privKey);
+ var resealed = SharedKeyGenerator.reseal(secretShared, request.ephemeralPubKey, KeyId.ofString("resealed-token")); // TODO key id
+
+ return new ResealingResponse(resealed.sealedSharedKey());
+ }
+
+
+ public SecretSharedKey openResealingResponse(ResealingResponse response) {
+ return SharedKeyGenerator.fromSealedKey(response.resealedKey, ephemeralKeyPair.getPrivate());
+ }
+
+}
diff --git a/security-utils/src/main/java/com/yahoo/security/hpke/Hpke.java b/security-utils/src/main/java/com/yahoo/security/hpke/Hpke.java
index 133798faa99..51f41ab7da7 100644
--- a/security-utils/src/main/java/com/yahoo/security/hpke/Hpke.java
+++ b/security-utils/src/main/java/com/yahoo/security/hpke/Hpke.java
@@ -175,7 +175,7 @@ public final class Hpke {
}
}
- private static record ContextBase(byte[] key, byte[] nonce, long seqNum, byte[] exporterSecret) { }
+ private record ContextBase(byte[] key, byte[] nonce, long seqNum, byte[] exporterSecret) { }
/**
* Section 5.1 Creating the Encryption Context:
@@ -219,8 +219,8 @@ public final class Hpke {
return new ContextBase(key, baseNonce, 0, exporterSecret);
}
- private static record ContextS(byte[] enc, ContextBase base) {}
- private static record ContextR(ContextBase base) {}
+ private record ContextS(byte[] enc, ContextBase base) {}
+ private record ContextR(ContextBase base) {}
/**
* Section 5.1.1 Encryption to a Public Key:
@@ -253,7 +253,7 @@ public final class Hpke {
return new ContextR(keySchedule(MODE_BASE, sharedSecret, info, DEFAULT_PSK, DEFAULT_PSK_ID));
}
- public static record Sealed(byte[] enc, byte[] ciphertext) {}
+ public record Sealed(byte[] enc, byte[] ciphertext) {}
/**
* Section 6.1 Encryption and Decryption:
diff --git a/security-utils/src/test/java/com/yahoo/security/SharedKeyTest.java b/security-utils/src/test/java/com/yahoo/security/SharedKeyTest.java
index 35b52d13b1d..875877aed6a 100644
--- a/security-utils/src/test/java/com/yahoo/security/SharedKeyTest.java
+++ b/security-utils/src/test/java/com/yahoo/security/SharedKeyTest.java
@@ -9,7 +9,7 @@ import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
-import java.util.Base64;
+import java.util.Optional;
import static com.yahoo.security.ArrayUtils.hex;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
@@ -232,6 +232,29 @@ public class SharedKeyTest {
assertEquals(terrifyingSecret, decrypted);
}
+ @Test
+ void shared_key_can_be_resealed_via_interactive_resealing_session() {
+ var originalReceiverKp = KeyUtils.generateX25519KeyPair();
+ var shared = SharedKeyGenerator.generateForReceiverPublicKey(originalReceiverKp.getPublic(), KEY_ID_1);
+ var secret = hex(shared.secretKey().getEncoded());
+
+ // Resealing requester side; ask for token to be resealed for ephemeral session public key
+ var session = SharedKeyResealingSession.newEphemeralSession();
+ var wrappedResealRequest = session.resealingRequestFor(shared.sealedSharedKey());
+
+ // Resealing request handler side; reseal using private key for original token
+ var unwrappedResealRequest = SharedKeyResealingSession.ResealingRequest.fromSerializedString(wrappedResealRequest.toSerializedString());
+ var wrappedResponse = SharedKeyResealingSession.reseal(unwrappedResealRequest,
+ (keyId) -> Optional.ofNullable(keyId.equals(KEY_ID_1) ? originalReceiverKp.getPrivate() : null));
+
+ // Back to resealing requester side
+ var unwrappedResponse = SharedKeyResealingSession.ResealingResponse.fromSerializedString(wrappedResponse.toSerializedString());
+ var resealed = session.openResealingResponse(unwrappedResponse);
+
+ var resealedSecret = hex(resealed.secretKey().getEncoded());
+ assertEquals(secret, resealedSecret);
+ }
+
// javax.crypto.CipherOutputStream swallows exceptions caused by MAC failures in cipher
// decryption mode (!) and must therefore _not_ be used for this purpose. This is documented,
// but still very surprising behavior.
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 4fbe89d4b03..4b3608fc3f7 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
@@ -2,14 +2,18 @@
package com.yahoo.vespa.security.tool.crypto;
import com.yahoo.security.SealedSharedKey;
+import com.yahoo.security.SecretSharedKey;
import com.yahoo.security.SharedKeyGenerator;
+import com.yahoo.security.SharedKeyResealingSession;
import com.yahoo.vespa.security.tool.CliUtils;
import com.yahoo.vespa.security.tool.Tool;
import com.yahoo.vespa.security.tool.ToolDescription;
import com.yahoo.vespa.security.tool.ToolInvocation;
import org.apache.commons.cli.Option;
+import java.io.BufferedReader;
import java.io.IOException;
+import java.io.InputStreamReader;
import java.util.List;
import java.util.Optional;
@@ -31,6 +35,7 @@ public class DecryptTool implements Tool {
static final String EXPECTED_KEY_ID_OPTION = "expected-key-id";
static final String ZSTD_DECOMPRESS_OPTION = "zstd-decompress";
static final String TOKEN_OPTION = "token";
+ static final String RESEAL_REQUEST = "reseal-request";
private static final List<Option> OPTIONS = List.of(
Option.builder("o")
@@ -77,6 +82,12 @@ public class DecryptTool implements Tool {
.hasArg(true)
.required(false)
.desc("Token generated when the input file was encrypted")
+ .build(),
+ Option.builder("r")
+ .longOpt(RESEAL_REQUEST)
+ .hasArg(false)
+ .required(false)
+ .desc("Delegate private key decryption via an interactive resealing session")
.build());
@Override
@@ -110,11 +121,12 @@ public class DecryptTool implements Tool {
var sealedSharedKey = SealedSharedKey.fromTokenString(tokenString.strip());
ToolUtils.verifyExpectedKeyId(sealedSharedKey, maybeKeyId);
- var privateKey = ToolUtils.resolvePrivateKeyFromInvocation(invocation, sealedSharedKey.keyId(),
- !CliUtils.useStdIo(inputArg) && !CliUtils.useStdIo(outputArg));
- var secretShared = SharedKeyGenerator.fromSealedKey(sealedSharedKey, privateKey);
- var cipher = secretShared.makeDecryptionCipher();
- boolean unZstd = arguments.hasOption(ZSTD_DECOMPRESS_OPTION);
+ var secret = arguments.hasOption(RESEAL_REQUEST)
+ ? secretFromInteractiveResealing(invocation, inputArg, outputArg, sealedSharedKey)
+ : secretFromPrivateKey(invocation, inputArg, outputArg, sealedSharedKey);
+
+ var cipher = secret.makeDecryptionCipher();
+ boolean unZstd = arguments.hasOption(ZSTD_DECOMPRESS_OPTION);
try (var inStream = CliUtils.inputStreamFromFileOrStream(inputArg, invocation.stdIn());
var outStream = CliUtils.outputStreamToFileOrStream(outputArg, invocation.stdOut())) {
@@ -125,4 +137,31 @@ public class DecryptTool implements Tool {
}
return 0;
}
+
+ private static SecretSharedKey secretFromPrivateKey(ToolInvocation invocation, String inputArg, String outputArg, SealedSharedKey sealedSharedKey) throws IOException {
+ var privateKey = ToolUtils.resolvePrivateKeyFromInvocation(invocation, sealedSharedKey.keyId(),
+ !CliUtils.useStdIo(inputArg) && !CliUtils.useStdIo(outputArg));
+ return SharedKeyGenerator.fromSealedKey(sealedSharedKey, privateKey);
+ }
+
+ private static SecretSharedKey secretFromInteractiveResealing(ToolInvocation invocation, String inputArg,
+ String outputArg, SealedSharedKey sealedSharedKey) throws IOException {
+ if (!CliUtils.useStdIo(outputArg) || !CliUtils.useStdIo(inputArg)) {
+ throw new IllegalArgumentException("Interactive token resealing not available with redirected I/O");
+ }
+ var session = SharedKeyResealingSession.newEphemeralSession();
+ var req = session.resealingRequestFor(sealedSharedKey);
+
+ invocation.stdOut().format("\nInteractive token resealing request:\n\n%s\n\n", req.toSerializedString());
+ invocation.stdOut().format("Paste response and hit return: ");
+
+ try (var reader = new BufferedReader(new InputStreamReader(invocation.stdIn()))) {
+ var serializedRes = reader.readLine().strip();
+ if (serializedRes.isEmpty()) {
+ throw new IllegalArgumentException("Empty response; aborting");
+ }
+ var res = SharedKeyResealingSession.ResealingResponse.fromSerializedString(serializedRes);
+ return session.openResealingResponse(res);
+ }
+ }
}
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 19be3e9fa51..4fb8083b0f0 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
@@ -5,10 +5,12 @@ import com.yahoo.security.KeyId;
import com.yahoo.security.KeyUtils;
import com.yahoo.security.SealedSharedKey;
import com.yahoo.security.SharedKeyGenerator;
+import com.yahoo.security.SharedKeyResealingSession;
import com.yahoo.vespa.security.tool.CliUtils;
import com.yahoo.vespa.security.tool.Tool;
import com.yahoo.vespa.security.tool.ToolDescription;
import com.yahoo.vespa.security.tool.ToolInvocation;
+import org.apache.commons.cli.CommandLine;
import org.apache.commons.cli.Option;
import java.io.IOException;
@@ -31,6 +33,7 @@ public class ResealTool implements Tool {
static final String EXPECTED_KEY_ID_OPTION = "expected-key-id";
static final String RECIPIENT_KEY_ID_OPTION = "key-id";
static final String RECIPIENT_PUBLIC_KEY_OPTION = "recipient-public-key";
+ static final String RESEAL_REQUEST_OPTION = "reseal-request";
private static final List<Option> OPTIONS = List.of(
Option.builder("k")
@@ -70,6 +73,12 @@ public class ResealTool implements Tool {
.hasArg(true)
.required(false)
.desc("ID of recipient key")
+ .build(),
+ Option.builder()
+ .longOpt(RESEAL_REQUEST_OPTION)
+ .hasArg(false)
+ .required(false)
+ .desc("Handle input as a resealing request instead of a token")
.build());
@Override
@@ -96,23 +105,41 @@ public class ResealTool implements Tool {
if (leftoverArgs.length != 1) {
throw new IllegalArgumentException("Expected exactly 1 token argument to re-seal");
}
- var tokenString = leftoverArgs[0];
- var maybeKeyId = Optional.ofNullable(arguments.hasOption(EXPECTED_KEY_ID_OPTION)
- ? arguments.getOptionValue(EXPECTED_KEY_ID_OPTION)
- : null);
- var sealedSharedKey = SealedSharedKey.fromTokenString(tokenString.strip());
- ToolUtils.verifyExpectedKeyId(sealedSharedKey, maybeKeyId);
-
- 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(), true);
- var secretShared = SharedKeyGenerator.fromSealedKey(sealedSharedKey, privateKey);
- var resealedShared = SharedKeyGenerator.reseal(secretShared, recipientPubKey, recipientKeyId);
-
- invocation.stdOut().println(resealedShared.sealedSharedKey().toTokenString());
+ var inputArg = leftoverArgs[0].strip();
+ var maybeKeyId = Optional.ofNullable(arguments.hasOption(EXPECTED_KEY_ID_OPTION)
+ ? arguments.getOptionValue(EXPECTED_KEY_ID_OPTION)
+ : null);
+ if (arguments.hasOption(RESEAL_REQUEST_OPTION)) {
+ handleResealingRequest(invocation, inputArg, maybeKeyId);
+ } else {
+ handleTokenResealing(invocation, arguments, inputArg, maybeKeyId);
+ }
} catch (IOException e) {
throw new RuntimeException(e);
}
return 0;
}
+
+ private static void handleTokenResealing(ToolInvocation invocation, CommandLine arguments, String inputArg, Optional<String> maybeKeyId) throws IOException {
+ var sealedSharedKey = SealedSharedKey.fromTokenString(inputArg);
+ ToolUtils.verifyExpectedKeyId(sealedSharedKey, maybeKeyId);
+
+ 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(), true);
+ var secretShared = SharedKeyGenerator.fromSealedKey(sealedSharedKey, privateKey);
+ var resealedShared = SharedKeyGenerator.reseal(secretShared, recipientPubKey, recipientKeyId);
+
+ invocation.stdOut().println(resealedShared.sealedSharedKey().toTokenString());
+ }
+
+ private static void handleResealingRequest(ToolInvocation invocation, String inputArg, Optional<String> maybeKeyId) throws IOException {
+ var request = SharedKeyResealingSession.ResealingRequest.fromSerializedString(inputArg);
+ ToolUtils.verifyExpectedKeyId(request.sealedKey(), maybeKeyId);
+
+ var privateKey = ToolUtils.resolvePrivateKeyFromInvocation(invocation, request.sealedKey().keyId(), true);
+ var resealed = SharedKeyResealingSession.reseal(request, (keyId) -> Optional.of(privateKey));
+
+ invocation.stdOut().println(resealed.toSerializedString());
+ }
}
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 ab47d11c602..a654f801f09 100644
--- a/vespaclient-java/src/test/resources/expected-decrypt-help-output.txt
+++ b/vespaclient-java/src/test/resources/expected-decrypt-help-output.txt
@@ -19,6 +19,8 @@ the quotes).
-o,--output-file <arg> Output file for decrypted plaintext.
Specify '-' (without the quotes) to write
plaintext to STDOUT instead of a file.
+ -r,--reseal-request Delegate private key decryption via an
+ interactive resealing session
-t,--token <arg> Token generated when the input file was
encrypted
-z,--zstd-decompress Decrypted data will be transparently
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 cb82bd434b4..b375147f58c 100644
--- a/vespaclient-java/src/test/resources/expected-reseal-help-output.txt
+++ b/vespaclient-java/src/test/resources/expected-reseal-help-output.txt
@@ -19,5 +19,7 @@ Prints new token to STDOUT.
a console
-r,--recipient-public-key <arg> Recipient X25519 public key in Base58
encoded format
+ --reseal-request Handle input as a resealing request
+ instead of a token
Note: this is a BETA tool version; its interface may be changed at any
time