diff --git a/settings.gradle b/settings.gradle index a636bb0..25d5c44 100644 --- a/settings.gradle +++ b/settings.gradle @@ -4,4 +4,5 @@ rootProject.name = 'VKS-Java' -include 'vks-java' \ No newline at end of file +include 'vks-java', + 'vks-java-cli' \ No newline at end of file diff --git a/vks-java-cli/build.gradle b/vks-java-cli/build.gradle new file mode 100644 index 0000000..4e96ef4 --- /dev/null +++ b/vks-java-cli/build.gradle @@ -0,0 +1,32 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +plugins { + id 'application' +} + +group 'org.pgpainless' + +repositories { + mavenCentral() +} + +dependencies { + testImplementation "org.junit.jupiter:junit-jupiter-api:$junitVersion" + testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:$junitVersion" + + // VKS-Java + implementation project(":vks-java") + + // CLI + implementation "info.picocli:picocli:4.6.3" +} + +application { + mainClass = 'pgp.vks.client.cli.VKSCLI' +} + +test { + useJUnitPlatform() +} \ No newline at end of file diff --git a/vks-java-cli/src/main/java/pgp/vks/client/cli/GetCmd.java b/vks-java-cli/src/main/java/pgp/vks/client/cli/GetCmd.java new file mode 100644 index 0000000..7f1fa9f --- /dev/null +++ b/vks-java-cli/src/main/java/pgp/vks/client/cli/GetCmd.java @@ -0,0 +1,74 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package pgp.vks.client.cli; + +import pgp.vks.client.Get; +import pgp.vks.client.VKS; +import picocli.CommandLine; + +import java.io.IOException; +import java.io.InputStream; +import java.net.MalformedURLException; + +@CommandLine.Command(name = "get", description = "Retrieve an OpenPGP certificate from the key server") +public class GetCmd implements Runnable { + + @CommandLine.Mixin + VKSCLI.KeyServerMixin keyServerMixin; + + @CommandLine.ArgGroup(exclusive = true, multiplicity = "1") + Exclusive by; + + static class Exclusive { + @CommandLine.Option(names = {"-f", "--by-fingerprint"}, description = "Retrieve a key by its fingerprint (NOT prefixed with '0x')") + String fingerprint; + + @CommandLine.Option(names = {"-i", "--by-keyid"}, description = "Retrieve a key by its decimal key ID or that of one of its subkeys.") + Long keyId; + + @CommandLine.Option(names = {"-e", "--by-email"}, description = "Retrieve a key by email address.") + String email; + } + + public void run() { + VKS vks; + try { + vks = keyServerMixin.parent.getApi(); + } catch (MalformedURLException e) { + throw new AssertionError(e); + } + + Get get = vks.get(); + InputStream inputStream = null; + try { + if (by.fingerprint != null) { + inputStream = get.byFingerprint(by.fingerprint); + } else if (by.keyId != null) { + inputStream = get.byKeyId(by.keyId); + } else if (by.email != null) { + inputStream = get.byEmail(by.email); + } else { + throw new IllegalArgumentException("Missing --by-* option."); + } + + int read; + byte[] buf = new byte[4096]; + while ((read = inputStream.read(buf)) != -1) { + System.out.write(buf, 0, read); + } + + } catch (IOException e) { + throw new AssertionError(e); + } finally { + if (inputStream != null) { + try { + inputStream.close(); + } catch (IOException e) { + throw new AssertionError(e); + } + } + } + } +} diff --git a/vks-java-cli/src/main/java/pgp/vks/client/cli/RequestVerificationCmd.java b/vks-java-cli/src/main/java/pgp/vks/client/cli/RequestVerificationCmd.java new file mode 100644 index 0000000..6767f7d --- /dev/null +++ b/vks-java-cli/src/main/java/pgp/vks/client/cli/RequestVerificationCmd.java @@ -0,0 +1,58 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package pgp.vks.client.cli; + +import pgp.vks.client.RequestVerify; +import pgp.vks.client.VKS; +import picocli.CommandLine; + +import java.io.IOException; +import java.net.MalformedURLException; +import java.util.Arrays; +import java.util.List; + +@CommandLine.Command(name = "request-verification", description = "Request verification for unverified user-ids") +public class RequestVerificationCmd implements Runnable { + + @CommandLine.Mixin + VKSCLI.KeyServerMixin keyServerMixin; + + @CommandLine.Option(names = {"-t", "--token"}, description = "Access token. Can be retrieved by uploading the certificate.", + required = true, arity = "1", paramLabel = "TOKEN") + String token; + + @CommandLine.Option(names = {"-l", "--locale"}, description = "Locale for the verification mail") + List locale = Arrays.asList("en_US", "en_GB"); + + @CommandLine.Option(names = {"-e", "--email"}, description = "Email addresses to request a verification mail for", required = true, arity = "1..*") + String[] addresses = new String[0]; + + + @Override + public void run() { + VKS vks; + try { + vks = keyServerMixin.parent.getApi(); + } catch (MalformedURLException e) { + throw new AssertionError(e); + } + + RequestVerify requestVerify = vks.requestVerification(); + try { + RequestVerify.Response response = requestVerify + .forEmailAddresses(addresses) + .execute(token, locale); + + System.out.println("Verification E-Mails for key " + response.getKeyFingerprint() + " have been sent."); + System.out.println("Token: " + response.getToken()); + System.out.println("Status:"); + for (String address : response.getStatus().keySet()) { + System.out.println("\t" + address + "\t" + response.getStatus().get(address)); + } + } catch (IOException e) { + throw new AssertionError(e); + } + } +} diff --git a/vks-java-cli/src/main/java/pgp/vks/client/cli/UploadCmd.java b/vks-java-cli/src/main/java/pgp/vks/client/cli/UploadCmd.java new file mode 100644 index 0000000..cba61bb --- /dev/null +++ b/vks-java-cli/src/main/java/pgp/vks/client/cli/UploadCmd.java @@ -0,0 +1,79 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package pgp.vks.client.cli; + +import pgp.vks.client.RequestVerify; +import pgp.vks.client.Status; +import pgp.vks.client.Upload; +import pgp.vks.client.VKS; +import pgp.vks.client.exception.CertCannotBePublishedException; +import picocli.CommandLine; + +import java.io.IOException; +import java.net.MalformedURLException; +import java.util.ArrayList; +import java.util.List; + +@CommandLine.Command(name = "upload", description = "Upload an OpenPGP certificate to the key server") +public class UploadCmd implements Runnable { + + @CommandLine.Mixin + VKSCLI.KeyServerMixin keyServerMixin; + + @CommandLine.Option(names = {"-r", "--request-verification"}, + description = "Request verification mails for unpublished email addresses") + boolean requestVerification; + + public void run() { + VKS vks; + try { + vks = keyServerMixin.parent.getApi(); + } catch (MalformedURLException e) { + throw new AssertionError(e); + } + + Upload upload = vks.upload(); + try { + Upload.Response response = upload.cert(System.in); + + // Unpublished mail addresses + List unpublished = new ArrayList<>(); + int maxMailLen = 0; + for (String address : response.getStatus().keySet()) { + Status status = response.getStatus().get(address); + if (address.length() > maxMailLen) { + maxMailLen = address.length(); + } + if (status != Status.published && status != Status.revoked) { + unpublished.add(address); + } + } + + System.out.println("Uploaded key " + response.getKeyFingerprint()); + System.out.println("Token: " + response.getToken()); + + if (!requestVerification || unpublished.isEmpty()) { + System.out.println("Status:"); + for (String address : response.getStatus().keySet()) { + Status status = response.getStatus().get(address); + System.out.format("%-" + maxMailLen + "s %s\n", address, status); + } + return; + } + + RequestVerify.Response verifyResponse = vks.requestVerification().forEmailAddresses(unpublished.toArray(new String[0])) + .execute(response.getToken()); + System.out.println("Status:"); + for (String address : verifyResponse.getStatus().keySet()) { + Status status = response.getStatus().get(address); + System.out.format("%-" + maxMailLen + "s %s\n", address, status); + } + } catch (CertCannotBePublishedException e) { + throw new AssertionError(e.getMessage()); + } catch (IOException e) { + throw new AssertionError(e); + } + } +} diff --git a/vks-java-cli/src/main/java/pgp/vks/client/cli/VKSCLI.java b/vks-java-cli/src/main/java/pgp/vks/client/cli/VKSCLI.java new file mode 100644 index 0000000..c5cba18 --- /dev/null +++ b/vks-java-cli/src/main/java/pgp/vks/client/cli/VKSCLI.java @@ -0,0 +1,60 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package pgp.vks.client.cli; + +import pgp.vks.client.VKS; +import pgp.vks.client.VKSImpl; +import picocli.CommandLine; + +import java.net.MalformedURLException; + +@CommandLine.Command( + subcommands = { + CommandLine.HelpCommand.class, + GetCmd.class, + UploadCmd.class, + RequestVerificationCmd.class + } +) +public class VKSCLI { + + String keyServer; + + public static void main(String[] args) { + int exitCode = execute(args); + if (exitCode != 0) { + System.exit(exitCode); + } + } + + public static int execute(String[] args) { + return new CommandLine(VKSCLI.class) + .setExitCodeExceptionMapper(new CommandLine.IExitCodeExceptionMapper() { + @Override + public int getExitCode(Throwable exception) { + return 1; + } + }) + .setCommandName("vkscli") + .execute(args); + } + + public VKS getApi() throws MalformedURLException { + return new VKSImpl(keyServer); + } + + public static class KeyServerMixin { + + @CommandLine.ParentCommand + VKSCLI parent; + + @CommandLine.Option(names = "--key-server", + description = "Address of the Verifying Key Server.\nDefaults to 'https://keys.openpgp.org'", + paramLabel = "KEYSERVER") + public void setKeyServer(String keyServer) { + parent.keyServer = keyServer; + } + } +} diff --git a/vks-java-cli/src/main/java/pgp/vks/client/cli/package-info.java b/vks-java-cli/src/main/java/pgp/vks/client/cli/package-info.java new file mode 100644 index 0000000..d6af2a8 --- /dev/null +++ b/vks-java-cli/src/main/java/pgp/vks/client/cli/package-info.java @@ -0,0 +1,8 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +/** + * Command Line Interface for VKS-Java. + */ +package pgp.vks.client.cli;