From 9d3ad01dfc757937dcd5454453725c896b857145 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 26 Nov 2020 11:00:48 +0100 Subject: [PATCH] Wip: Start implementing a SOP client --- pgpainless-sop/build.gradle | 30 ++++++++ .../org/pgpainless/sop/PGPainlessCLI.java | 48 +++++++++++++ .../main/java/org/pgpainless/sop/Print.java | 16 +++++ .../pgpainless/sop/commands/ExtractCert.java | 35 +++++++++ .../pgpainless/sop/commands/GenerateKey.java | 39 ++++++++++ .../org/pgpainless/sop/commands/Sign.java | 72 +++++++++++++++++++ .../org/pgpainless/sop/commands/Version.java | 12 ++++ settings.gradle | 3 +- 8 files changed, 254 insertions(+), 1 deletion(-) create mode 100644 pgpainless-sop/build.gradle create mode 100644 pgpainless-sop/src/main/java/org/pgpainless/sop/PGPainlessCLI.java create mode 100644 pgpainless-sop/src/main/java/org/pgpainless/sop/Print.java create mode 100644 pgpainless-sop/src/main/java/org/pgpainless/sop/commands/ExtractCert.java create mode 100644 pgpainless-sop/src/main/java/org/pgpainless/sop/commands/GenerateKey.java create mode 100644 pgpainless-sop/src/main/java/org/pgpainless/sop/commands/Sign.java create mode 100644 pgpainless-sop/src/main/java/org/pgpainless/sop/commands/Version.java diff --git a/pgpainless-sop/build.gradle b/pgpainless-sop/build.gradle new file mode 100644 index 00000000..f7a8cf6e --- /dev/null +++ b/pgpainless-sop/build.gradle @@ -0,0 +1,30 @@ +plugins { + id 'com.github.johnrengelman.shadow' version '1.2.2' +} + +apply plugin: 'com.github.johnrengelman.shadow' + +dependencies { + implementation(project(":pgpainless-core")) + + compile 'info.picocli:picocli:3.9.6' + + testImplementation "org.junit.jupiter:junit-jupiter-api:$junitVersion" + testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:$junitVersion" + + /* + implementation "org.bouncycastle:bcprov-debug-jdk15on:$bouncyCastleVersion" + /*/ + implementation "org.bouncycastle:bcprov-jdk15on:$bouncyCastleVersion" + //*/ + api "org.bouncycastle:bcpg-jdk15on:$bouncyCastleVersion" + + // https://mvnrepository.com/artifact/com.google.code.findbugs/jsr305 + implementation group: 'com.google.code.findbugs', name: 'jsr305', version: '3.0.2' +} + +mainClassName = 'org.pgpainless.sop.PGPainlessCLI' + +shadowJar { + baseName = 'pgpcli' +} diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/PGPainlessCLI.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/PGPainlessCLI.java new file mode 100644 index 00000000..e0a25c5a --- /dev/null +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/PGPainlessCLI.java @@ -0,0 +1,48 @@ +package org.pgpainless.sop; + +import org.pgpainless.sop.commands.ExtractCert; +import org.pgpainless.sop.commands.GenerateKey; +import org.pgpainless.sop.commands.Sign; +import org.pgpainless.sop.commands.Version; +import picocli.CommandLine; + +@CommandLine.Command( + subcommands = { + Version.class, + GenerateKey.class, + ExtractCert.class, + Sign.class + } +) +public class PGPainlessCLI implements Runnable { + + public static void main(String[] args) { + interpret(args); + // generateKey(); + } + + public static void interpret(String... args) { + CommandLine.run(new PGPainlessCLI(), args); + } + + private static void version() { + CommandLine.run(new PGPainlessCLI(), "version"); + } + + private static void generateKey() { + interpret("generate-key", "--armor", "Alice Example "); + } + + private static void extractCert() { + CommandLine.run(new PGPainlessCLI(), "extract-cert"); + } + + private static void sign() { + interpret("sign", "--armor", "--as=text", "alice.sec"); + } + + @Override + public void run() { + + } +} diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/Print.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/Print.java new file mode 100644 index 00000000..2dee1679 --- /dev/null +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/Print.java @@ -0,0 +1,16 @@ +package org.pgpainless.sop; + +import java.io.IOException; + +import org.pgpainless.util.ArmorUtils; + +public class Print { + + public static String toString(byte[] bytes, boolean armor) throws IOException { + if (armor) { + return ArmorUtils.toAsciiArmoredString(bytes); + } else { + return new String(bytes, "UTF-8"); + } + } +} diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/commands/ExtractCert.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/commands/ExtractCert.java new file mode 100644 index 00000000..e4b72484 --- /dev/null +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/commands/ExtractCert.java @@ -0,0 +1,35 @@ +package org.pgpainless.sop.commands; + +import java.io.IOException; + +import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.PGPPublicKeyRing; +import org.bouncycastle.openpgp.PGPSecretKeyRing; +import org.pgpainless.PGPainless; +import org.pgpainless.sop.Print; +import org.pgpainless.util.BCUtil; +import picocli.CommandLine; + +@CommandLine.Command(name = "extract-cert") +public class ExtractCert implements Runnable { + + @CommandLine.Option(names = {"--armor"}, description = "ASCII Armor the output") + boolean armor = false; + + @CommandLine.Option(names = {"--no-armor"}) + boolean noArmor = false; + + @Override + public void run() { + try { + PGPSecretKeyRing secretKeys = PGPainless.readKeyRing().secretKeyRing(System.in); + PGPPublicKeyRing publicKeys = BCUtil.publicKeyRingFromSecretKeyRing(secretKeys); + + System.out.println(Print.toString(publicKeys.getEncoded(), !noArmor)); + } catch (IOException | PGPException e) { + System.err.println("Error extracting certificate from keys;"); + System.err.println(e.getMessage()); + System.exit(1); + } + } +} diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/commands/GenerateKey.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/commands/GenerateKey.java new file mode 100644 index 00000000..7999f88a --- /dev/null +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/commands/GenerateKey.java @@ -0,0 +1,39 @@ +package org.pgpainless.sop.commands; + +import java.io.IOException; +import java.security.InvalidAlgorithmParameterException; +import java.security.NoSuchAlgorithmException; + +import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.PGPSecretKeyRing; +import org.pgpainless.PGPainless; +import org.pgpainless.sop.Print; +import org.pgpainless.util.ArmorUtils; +import picocli.CommandLine; + +@CommandLine.Command(name = "generate-key") +public class GenerateKey implements Runnable { + + @CommandLine.Option(names = {"--armor"}, description = "ASCII Armor the output") + boolean armor = false; + + @CommandLine.Option(names = {"--no-armor"}) + boolean noArmor = false; + + @CommandLine.Parameters + String userId; + + @Override + public void run() { + try { + PGPSecretKeyRing secretKeys = PGPainless.generateKeyRing().simpleEcKeyRing(userId).getSecretKeys(); + + System.out.println(Print.toString(secretKeys.getEncoded(), !noArmor)); + + } catch (InvalidAlgorithmParameterException | NoSuchAlgorithmException | PGPException | IOException e) { + System.err.println("Error creating OpenPGP key:"); + System.err.println(e.getMessage()); + System.exit(1); + } + } +} diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/commands/Sign.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/commands/Sign.java new file mode 100644 index 00000000..e2a61d69 --- /dev/null +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/commands/Sign.java @@ -0,0 +1,72 @@ +package org.pgpainless.sop.commands; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; + +import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.PGPSecretKeyRing; +import org.bouncycastle.openpgp.PGPSignature; +import org.bouncycastle.util.io.Streams; +import org.pgpainless.PGPainless; +import org.pgpainless.encryption_signing.EncryptionStream; +import org.pgpainless.key.protection.UnprotectedKeysProtector; +import org.pgpainless.sop.Print; +import picocli.CommandLine; + +@CommandLine.Command(name = "sign") +public class Sign implements Runnable { + + public enum Type { + binary, + text + } + + @CommandLine.Option(names = {"--armor"}, description = "ASCII Armor the output") + boolean armor = false; + + @CommandLine.Option(names = {"--no-armor"}) + boolean noArmor = false; + + @CommandLine.Option(names = "--as", description = "Defaults to 'binary'. If '--as=text' and the input data is not valid UTF-8, sign fails with return code 53.") + Type type; + + @CommandLine.Parameters + File secretKeyFile; + + @Override + public void run() { + PGPSecretKeyRing secretKeys; + try { + secretKeys = PGPainless.readKeyRing().secretKeyRing(new FileInputStream(secretKeyFile)); + } catch (IOException | PGPException e) { + System.err.println("Error reading secret key ring."); + System.err.println(e.getMessage()); + + System.exit(1); + return; + } + try { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + EncryptionStream encryptionStream = PGPainless.createEncryptor() + .onOutputStream(out) + .doNotEncrypt() + .createDetachedSignature() + .signWith(new UnprotectedKeysProtector(), secretKeys) + .noArmor(); + + Streams.pipeAll(System.in, encryptionStream); + encryptionStream.close(); + + PGPSignature signature = encryptionStream.getResult().getSignatures().iterator().next(); + + System.out.println(Print.toString(signature.getEncoded(), !noArmor)); + } catch (PGPException | IOException e) { + System.err.println("Error signing data."); + System.err.println(e.getMessage()); + + System.exit(1); + } + } +} diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/commands/Version.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/commands/Version.java new file mode 100644 index 00000000..25a646b2 --- /dev/null +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/commands/Version.java @@ -0,0 +1,12 @@ +package org.pgpainless.sop.commands; + +import picocli.CommandLine; + +@CommandLine.Command(name = "version") +public class Version implements Runnable { + + @Override + public void run() { + System.out.println("PGPainless CLI version 0.0.1"); + } +} diff --git a/settings.gradle b/settings.gradle index b3ba260c..5cc5fd5c 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,4 +1,5 @@ rootProject.name = 'PGPainless' -include 'pgpainless-core' +include 'pgpainless-core', + 'pgpainless-sop'