summaryrefslogtreecommitdiffstats
path: root/vespaclient-java/src/main/java/com/yahoo/vespa/security/tool/crypto
diff options
context:
space:
mode:
Diffstat (limited to 'vespaclient-java/src/main/java/com/yahoo/vespa/security/tool/crypto')
-rw-r--r--vespaclient-java/src/main/java/com/yahoo/vespa/security/tool/crypto/CipherUtils.java37
-rw-r--r--vespaclient-java/src/main/java/com/yahoo/vespa/security/tool/crypto/DecryptTool.java111
-rw-r--r--vespaclient-java/src/main/java/com/yahoo/vespa/security/tool/crypto/EncryptTool.java93
-rw-r--r--vespaclient-java/src/main/java/com/yahoo/vespa/security/tool/crypto/KeygenTool.java105
4 files changed, 346 insertions, 0 deletions
diff --git a/vespaclient-java/src/main/java/com/yahoo/vespa/security/tool/crypto/CipherUtils.java b/vespaclient-java/src/main/java/com/yahoo/vespa/security/tool/crypto/CipherUtils.java
new file mode 100644
index 00000000000..e3954558026
--- /dev/null
+++ b/vespaclient-java/src/main/java/com/yahoo/vespa/security/tool/crypto/CipherUtils.java
@@ -0,0 +1,37 @@
+// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.security.tool.crypto;
+
+import javax.crypto.Cipher;
+import javax.crypto.CipherOutputStream;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+
+/**
+ * @author vekterli
+ */
+public class CipherUtils {
+
+ /**
+ * Streams the contents of fromPath into toPath after being wrapped by the input cipher.
+ * Depending on the Cipher mode, this either encrypts a plaintext file into ciphertext,
+ * or decrypts a ciphertext file into plaintext.
+ *
+ * @param fromPath source file path to read from
+ * @param toPath destination file path to write to
+ * @param cipher a Cipher in either ENCRYPT or DECRYPT mode
+ * @throws IOException if any file operation fails
+ */
+ public static void streamEncipherFileContents(Path fromPath, Path toPath, Cipher cipher) throws IOException {
+ if (fromPath.equals(toPath)) {
+ throw new IllegalArgumentException("Can't use same file as both input and output for enciphering");
+ }
+ try (var inStream = Files.newInputStream(fromPath);
+ var outStream = Files.newOutputStream(toPath);
+ var cipherStream = new CipherOutputStream(outStream, cipher)) {
+ inStream.transferTo(cipherStream);
+ cipherStream.flush();
+ }
+ }
+
+}
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
new file mode 100644
index 00000000000..b307ab76da8
--- /dev/null
+++ b/vespaclient-java/src/main/java/com/yahoo/vespa/security/tool/crypto/DecryptTool.java
@@ -0,0 +1,111 @@
+// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.security.tool.crypto;
+
+import com.yahoo.security.KeyUtils;
+import com.yahoo.security.SealedSharedKey;
+import com.yahoo.security.SharedKeyGenerator;
+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.IOException;
+import java.nio.file.Files;
+import java.nio.file.Paths;
+import java.util.List;
+import java.util.Optional;
+
+/**
+ * Tooling for decrypting a file using a private key that corresponds to the public key used
+ * to originally encrypt the file.
+ *
+ * Uses the opaque token abstraction from {@link SharedKeyGenerator}.
+ *
+ * @author vekterli
+ */
+public class DecryptTool implements Tool {
+
+ static final String OUTPUT_FILE_OPTION = "output-file";
+ static final String RECIPIENT_PRIVATE_KEY_FILE_OPTION = "recipient-private-key-file";
+ static final String KEY_ID_OPTION = "key-id";
+ static final String TOKEN_OPTION = "token";
+
+ private static final List<Option> OPTIONS = List.of(
+ Option.builder("o")
+ .longOpt(OUTPUT_FILE_OPTION)
+ .hasArg(true)
+ .required(false)
+ .desc("Output file for decrypted plaintext")
+ .build(),
+ Option.builder("k")
+ .longOpt(RECIPIENT_PRIVATE_KEY_FILE_OPTION)
+ .hasArg(true)
+ .required(false)
+ .desc("Recipient private key file")
+ .build(),
+ Option.builder("i")
+ .longOpt(KEY_ID_OPTION)
+ .hasArg(true)
+ .required(false)
+ .desc("Numeric ID of recipient key. If this is not provided, " +
+ "the key ID stored as part of the token is not verified.")
+ .build(),
+ Option.builder("t")
+ .longOpt(TOKEN_OPTION)
+ .hasArg(true)
+ .required(false)
+ .desc("Token generated when the input file was encrypted")
+ .build());
+
+ @Override
+ public String name() {
+ return "decrypt";
+ }
+
+ @Override
+ public ToolDescription description() {
+ return new ToolDescription(
+ "<encrypted file> <options>",
+ "Decrypts a file using a provided token and a secret private key. The file must " +
+ "previously have been encrypted using the public key component of the given private key.",
+ "Note: this is a BETA tool version; its interface may be changed at any time",
+ OPTIONS);
+ }
+
+ @Override
+ public int invoke(ToolInvocation invocation) {
+ try {
+ var arguments = invocation.arguments();
+ var leftoverArgs = arguments.getArgs();
+ if (leftoverArgs.length != 1) {
+ throw new IllegalArgumentException("Expected exactly 1 file argument to decrypt");
+ }
+ var inputPath = Paths.get(leftoverArgs[0]);
+ if (!inputPath.toFile().exists()) {
+ throw new IllegalArgumentException("Cannot decrypt file '%s' as it does not exist".formatted(inputPath.toString()));
+ }
+ var maybeKeyId = Optional.ofNullable(arguments.hasOption(KEY_ID_OPTION)
+ ? Integer.parseInt(arguments.getOptionValue(KEY_ID_OPTION))
+ : null);
+ var outputPath = Paths.get(CliUtils.optionOrThrow(arguments, OUTPUT_FILE_OPTION));
+ var privKeyPath = Paths.get(CliUtils.optionOrThrow(arguments, RECIPIENT_PRIVATE_KEY_FILE_OPTION));
+ var tokenString = CliUtils.optionOrThrow(arguments, TOKEN_OPTION);
+ var sealedSharedKey = SealedSharedKey.fromTokenString(tokenString.strip());
+ if (maybeKeyId.isPresent() && (maybeKeyId.get() != sealedSharedKey.keyId())) {
+ throw new IllegalArgumentException(("Key ID specified with --key-id (%d) does not match key ID " +
+ "used when generating the supplied token (%d)")
+ .formatted(maybeKeyId.get(), sealedSharedKey.keyId()));
+ }
+ var privateKey = KeyUtils.fromBase64EncodedX25519PrivateKey(Files.readString(privKeyPath).strip());
+ var secretShared = SharedKeyGenerator.fromSealedKey(sealedSharedKey, privateKey);
+ var cipher = SharedKeyGenerator.makeAesGcmDecryptionCipher(secretShared);
+
+ CipherUtils.streamEncipherFileContents(inputPath, outputPath, cipher);
+
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ return 0;
+ }
+}
diff --git a/vespaclient-java/src/main/java/com/yahoo/vespa/security/tool/crypto/EncryptTool.java b/vespaclient-java/src/main/java/com/yahoo/vespa/security/tool/crypto/EncryptTool.java
new file mode 100644
index 00000000000..5d6cee2fabc
--- /dev/null
+++ b/vespaclient-java/src/main/java/com/yahoo/vespa/security/tool/crypto/EncryptTool.java
@@ -0,0 +1,93 @@
+// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.security.tool.crypto;
+
+import com.yahoo.security.KeyUtils;
+import com.yahoo.security.SharedKeyGenerator;
+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.IOException;
+import java.nio.file.Paths;
+import java.util.List;
+
+/**
+ * Tooling to encrypt a file using a public key, emitting a non-secret token that can be
+ * passed on to a recipient holding the corresponding private key.
+ *
+ * Uses the opaque token abstraction from {@link SharedKeyGenerator}.
+ *
+ * @author vekterli
+ */
+public class EncryptTool implements Tool {
+
+ static final String OUTPUT_FILE_OPTION = "output-file";
+ static final String KEY_ID_OPTION = "key-id";
+ static final String RECIPIENT_PUBLIC_KEY_OPTION = "recipient-public-key";
+
+ private static final List<Option> OPTIONS = List.of(
+ Option.builder("o")
+ .longOpt(OUTPUT_FILE_OPTION)
+ .hasArg(true)
+ .required(false)
+ .desc("Output file (will be truncated if it already exists)")
+ .build(),
+ Option.builder("r")
+ .longOpt(RECIPIENT_PUBLIC_KEY_OPTION)
+ .hasArg(true)
+ .required(false)
+ .desc("Recipient X25519 public key in Base64 encoded format")
+ .build(),
+ Option.builder("i")
+ .longOpt(KEY_ID_OPTION)
+ .hasArg(true)
+ .required(false)
+ .desc("Numeric ID of recipient key")
+ .build());
+
+ @Override
+ public String name() {
+ return "encrypt";
+ }
+
+ @Override
+ public ToolDescription description() {
+ return new ToolDescription(
+ "<input file> <options>",
+ "One-way encrypts a file using the public key of a recipient. A public token is printed on " +
+ "standard out. The recipient can use this token to decrypt the file using their private key. " +
+ "The token does not have to be kept secret.",
+ "Note: this is a BETA tool version; its interface may be changed at any time",
+ OPTIONS);
+ }
+
+ @Override
+ public int invoke(ToolInvocation invocation) {
+ try {
+ var arguments = invocation.arguments();
+ var leftoverArgs = arguments.getArgs();
+ if (leftoverArgs.length != 1) {
+ throw new IllegalArgumentException("Expected exactly 1 file argument to encrypt");
+ }
+ var inputPath = Paths.get(leftoverArgs[0]);
+ if (!inputPath.toFile().exists()) {
+ throw new IllegalArgumentException("Cannot encrypt file '%s' as it does not exist".formatted(inputPath.toString()));
+ }
+ var outputPath = Paths.get(CliUtils.optionOrThrow(arguments, OUTPUT_FILE_OPTION));
+
+ var recipientPubKey = KeyUtils.fromBase64EncodedX25519PublicKey(CliUtils.optionOrThrow(arguments, RECIPIENT_PUBLIC_KEY_OPTION).strip());
+ int keyId = Integer.parseInt(CliUtils.optionOrThrow(arguments, KEY_ID_OPTION));
+ var shared = SharedKeyGenerator.generateForReceiverPublicKey(recipientPubKey, keyId);
+ var cipher = SharedKeyGenerator.makeAesGcmEncryptionCipher(shared);
+
+ CipherUtils.streamEncipherFileContents(inputPath, outputPath, cipher);
+
+ invocation.stdOut().println(shared.sealedSharedKey().toTokenString());
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ return 0;
+ }
+}
diff --git a/vespaclient-java/src/main/java/com/yahoo/vespa/security/tool/crypto/KeygenTool.java b/vespaclient-java/src/main/java/com/yahoo/vespa/security/tool/crypto/KeygenTool.java
new file mode 100644
index 00000000000..a0b9cce710b
--- /dev/null
+++ b/vespaclient-java/src/main/java/com/yahoo/vespa/security/tool/crypto/KeygenTool.java
@@ -0,0 +1,105 @@
+// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.security.tool.crypto;
+
+import com.yahoo.security.KeyUtils;
+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.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.nio.file.attribute.PosixFilePermissions;
+import java.security.interfaces.XECPrivateKey;
+import java.security.interfaces.XECPublicKey;
+import java.util.List;
+
+/**
+ * Tooling to generate random X25519 key pairs.
+ *
+ * @author vekterli
+ */
+public class KeygenTool implements Tool {
+
+ static final String PRIVATE_OUT_FILE_OPTION = "private-out-file";
+ static final String PUBLIC_OUT_FILE_OPTION = "public-out-file";
+ static final String OVERWRITE_EXISTING_OPTION = "overwrite-existing";
+
+ private static final List<Option> OPTIONS = List.of(
+ Option.builder("k")
+ .longOpt(PRIVATE_OUT_FILE_OPTION)
+ .hasArg(true)
+ .required(false)
+ .desc("Output file for private (secret) key. Will be created with restrictive file permissions.")
+ .build(),
+ Option.builder("p")
+ .longOpt(PUBLIC_OUT_FILE_OPTION)
+ .hasArg(true)
+ .required(false)
+ .desc("Output file for public key")
+ .build(),
+ Option.builder()
+ .longOpt(OVERWRITE_EXISTING_OPTION)
+ .hasArg(false)
+ .required(false)
+ .desc("Overwrite existing key files instead of failing key generation if " +
+ "any files already exist. Use with great caution!")
+ .build());
+
+ @Override
+ public String name() {
+ return "keygen";
+ }
+
+ @Override
+ public ToolDescription description() {
+ return new ToolDescription(
+ "<options>",
+ "Generates an X25519 key pair and stores its private/public parts in " +
+ "separate files in Base64 encoded form.",
+ "Note: this is a BETA tool version; its interface may be changed at any time",
+ OPTIONS);
+ }
+
+ private void handleExistingFileIfAny(Path filePath, boolean allowOverwrite) throws IOException {
+ if (filePath.toFile().exists()) {
+ if (!allowOverwrite) {
+ throw new IllegalArgumentException(("Output file '%s' already exists. No keys written. " +
+ "If you want to overwrite existing files, specify --%s.")
+ .formatted(filePath.toAbsolutePath().toString(), OVERWRITE_EXISTING_OPTION));
+ } else {
+ // Explicitly delete the file since Files.createFile() will fail if it already exists.
+ Files.delete(filePath);
+ }
+ }
+ }
+
+ @Override
+ public int invoke(ToolInvocation invocation) {
+ try {
+ var arguments = invocation.arguments();
+ var privOutPath = Paths.get(CliUtils.optionOrThrow(arguments, PRIVATE_OUT_FILE_OPTION));
+ var pubOutPath = Paths.get(CliUtils.optionOrThrow(arguments, PUBLIC_OUT_FILE_OPTION));
+
+ boolean allowOverwrite = arguments.hasOption(OVERWRITE_EXISTING_OPTION);
+ handleExistingFileIfAny(privOutPath, allowOverwrite);
+ handleExistingFileIfAny(pubOutPath, allowOverwrite);
+
+ var keyPair = KeyUtils.generateX25519KeyPair();
+ var privKey = (XECPrivateKey) keyPair.getPrivate();
+ var pubKey = (XECPublicKey) keyPair.getPublic();
+
+ var privFilePerms = PosixFilePermissions.fromString("rw-------");
+ Files.createFile( privOutPath, PosixFilePermissions.asFileAttribute(privFilePerms));
+ Files.writeString(privOutPath, KeyUtils.toBase64EncodedX25519PrivateKey(privKey) + "\n");
+ Files.writeString(pubOutPath, KeyUtils.toBase64EncodedX25519PublicKey(pubKey) + "\n");
+
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ return 0;
+ }
+}