summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-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