From 9d3ad01dfc757937dcd5454453725c896b857145 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 26 Nov 2020 11:00:48 +0100 Subject: [PATCH 01/25] 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' From 379ad285f3fea3108868f090a517d9d0c10c533b Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Sun, 6 Dec 2020 16:52:08 +0100 Subject: [PATCH 02/25] Fix gradle build file --- pgpainless-sop/build.gradle | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/pgpainless-sop/build.gradle b/pgpainless-sop/build.gradle index f7a8cf6e..ce98b188 100644 --- a/pgpainless-sop/build.gradle +++ b/pgpainless-sop/build.gradle @@ -1,9 +1,7 @@ plugins { - id 'com.github.johnrengelman.shadow' version '1.2.2' + id 'application' } -apply plugin: 'com.github.johnrengelman.shadow' - dependencies { implementation(project(":pgpainless-core")) @@ -17,7 +15,7 @@ dependencies { /*/ implementation "org.bouncycastle:bcprov-jdk15on:$bouncyCastleVersion" //*/ - api "org.bouncycastle:bcpg-jdk15on:$bouncyCastleVersion" + implementation "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' @@ -25,6 +23,17 @@ dependencies { mainClassName = 'org.pgpainless.sop.PGPainlessCLI' -shadowJar { - baseName = 'pgpcli' +jar { + manifest { + attributes 'Main-Class': "$mainClassName" + } + + from { + configurations.runtimeClasspath.collect { it.isDirectory() ? it : zipTree(it) } + } { + exclude "META-INF/*.SF" + exclude "META-INF/*.DSA" + exclude "META-INF/*.RSA" + } } + From 6f48a1a2610ca87b3d280dde0df697b9fe235529 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 16 Dec 2020 16:09:16 +0100 Subject: [PATCH 03/25] Apply latest changes to master --- .../src/main/java/org/pgpainless/sop/commands/GenerateKey.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 index 7999f88a..225cca9c 100644 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/commands/GenerateKey.java +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/commands/GenerateKey.java @@ -26,7 +26,7 @@ public class GenerateKey implements Runnable { @Override public void run() { try { - PGPSecretKeyRing secretKeys = PGPainless.generateKeyRing().simpleEcKeyRing(userId).getSecretKeys(); + PGPSecretKeyRing secretKeys = PGPainless.generateKeyRing().simpleEcKeyRing(userId); System.out.println(Print.toString(secretKeys.getEncoded(), !noArmor)); From e07998f99d6750c4be3af3f03c48652a2312e7ef Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 16 Dec 2020 16:43:58 +0100 Subject: [PATCH 04/25] SOP: Respect signature type --- .../java/org/pgpainless/sop/commands/Sign.java | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) 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 index e2a61d69..5568ced7 100644 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/commands/Sign.java +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/commands/Sign.java @@ -10,6 +10,7 @@ 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.EncryptionBuilderInterface; import org.pgpainless.encryption_signing.EncryptionStream; import org.pgpainless.key.protection.UnprotectedKeysProtector; import org.pgpainless.sop.Print; @@ -48,13 +49,15 @@ public class Sign implements Runnable { return; } try { - ByteArrayOutputStream out = new ByteArrayOutputStream(); - EncryptionStream encryptionStream = PGPainless.createEncryptor() - .onOutputStream(out) - .doNotEncrypt() - .createDetachedSignature() - .signWith(new UnprotectedKeysProtector(), secretKeys) - .noArmor(); + ByteArrayOutputStream out = new ByteArrayOutputStream(); + EncryptionBuilderInterface.DocumentType documentType = PGPainless.encryptAndOrSign() + .onOutputStream(out) + .doNotEncrypt() + .createDetachedSignature() + .signWith(new UnprotectedKeysProtector(), secretKeys); + + EncryptionBuilderInterface.Armor armor = type == Type.text ? documentType.signCanonicalText() : documentType.signBinaryDocument(); + EncryptionStream encryptionStream = noArmor ? armor.noArmor() : armor.asciiArmor(); Streams.pipeAll(System.in, encryptionStream); encryptionStream.close(); From eb9587163d1512cedad24d6b5401936708bb0f3d Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 16 Dec 2020 17:31:27 +0100 Subject: [PATCH 05/25] SOP: Implement basic signature verification --- .../org/pgpainless/sop/PGPainlessCLI.java | 8 +- .../org/pgpainless/sop/commands/Verify.java | 90 +++++++++++++++++++ 2 files changed, 93 insertions(+), 5 deletions(-) create mode 100644 pgpainless-sop/src/main/java/org/pgpainless/sop/commands/Verify.java diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/PGPainlessCLI.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/PGPainlessCLI.java index e0a25c5a..dcab8773 100644 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/PGPainlessCLI.java +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/PGPainlessCLI.java @@ -1,9 +1,6 @@ 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 org.pgpainless.sop.commands.*; import picocli.CommandLine; @CommandLine.Command( @@ -11,7 +8,8 @@ import picocli.CommandLine; Version.class, GenerateKey.class, ExtractCert.class, - Sign.class + Sign.class, + Verify.class } ) public class PGPainlessCLI implements Runnable { diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/commands/Verify.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/commands/Verify.java new file mode 100644 index 00000000..66fef9e1 --- /dev/null +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/commands/Verify.java @@ -0,0 +1,90 @@ +package org.pgpainless.sop.commands; + +import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.PGPPublicKeyRing; +import org.bouncycastle.openpgp.PGPSignature; +import org.bouncycastle.util.io.Streams; +import org.pgpainless.PGPainless; +import org.pgpainless.decryption_verification.DecryptionStream; +import org.pgpainless.decryption_verification.OpenPgpMetadata; +import org.pgpainless.key.OpenPgpV4Fingerprint; +import picocli.CommandLine; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.text.DateFormat; +import java.text.SimpleDateFormat; +import java.util.*; + +@CommandLine.Command(name = "verify") +public class Verify implements Runnable { + + @CommandLine.Parameters(index = "0", description = "The detached signature") + File signature; + + @CommandLine.Parameters(index = "1..*") + File[] certs; + + TimeZone tz = TimeZone.getTimeZone("UTC"); + DateFormat df = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm'Z'"); + + @Override + public void run() { + df.setTimeZone(tz); + + if (certs.length == 0) { + System.out.println("No certificates supplied."); + System.exit(19); + } + + Map publicKeys = new HashMap<>(); + for (File cert : certs) { + try(FileInputStream in = new FileInputStream(cert)) { + publicKeys.put(cert, PGPainless.readKeyRing().publicKeyRing(in)); + } catch (IOException e) { + e.printStackTrace(); + } + } + + try(FileInputStream sigIn = new FileInputStream(signature)) { + DecryptionStream verifier = PGPainless.decryptAndOrVerify() + .onInputStream(System.in) + .doNotDecrypt() + .verifyDetachedSignature(sigIn) + .verifyWith(new HashSet<>(publicKeys.values())) + .ignoreMissingPublicKeys() + .build(); + + ByteArrayOutputStream out = new ByteArrayOutputStream(); + Streams.pipeAll(verifier, out); + verifier.close(); + + OpenPgpMetadata metadata = verifier.getResult(); + + for (OpenPgpV4Fingerprint sigKeyFp : metadata.getVerifiedSignatures().keySet()) { + PGPSignature signature = metadata.getVerifiedSignatures().get(sigKeyFp); + for (File file : certs) { + // Search signing key ring + PGPPublicKeyRing publicKeyRing = publicKeys.get(file); + if (publicKeyRing.getPublicKey(sigKeyFp.getKeyId()) == null) { + continue; + } + + String utcSigDate = df.format(signature.getCreationTime()); + OpenPgpV4Fingerprint primaryKeyFp = new OpenPgpV4Fingerprint(publicKeyRing); + System.out.println(utcSigDate + " " + sigKeyFp.toString() + " " + primaryKeyFp.toString() + + " signed by " + file.getName()); + } + } + + if (metadata.getVerifiedSignatures().isEmpty()) { + System.out.println("Signature validation failed."); + System.exit(3); + } + } catch (IOException | PGPException e) { + e.printStackTrace(); + } + } +} From 930448b02bffc5ccfa636c9fdf1dcc72266bc398 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 16 Dec 2020 18:35:25 +0100 Subject: [PATCH 06/25] SOP: Add documentation and parse not-{before|after} dates in verify --- .../pgpainless/sop/commands/GenerateKey.java | 4 +- .../org/pgpainless/sop/commands/Verify.java | 114 +++++++++++++----- .../org/pgpainless/sop/commands/Version.java | 2 +- 3 files changed, 87 insertions(+), 33 deletions(-) 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 index 225cca9c..745411d7 100644 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/commands/GenerateKey.java +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/commands/GenerateKey.java @@ -17,10 +17,10 @@ public class GenerateKey implements Runnable { @CommandLine.Option(names = {"--armor"}, description = "ASCII Armor the output") boolean armor = false; - @CommandLine.Option(names = {"--no-armor"}) + @CommandLine.Option(names = {"--no-armor"}, description = "Non-armored, binary output") boolean noArmor = false; - @CommandLine.Parameters + @CommandLine.Parameters(description = "User-ID, eg. \"Alice \"") String userId; @Override diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/commands/Verify.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/commands/Verify.java index 66fef9e1..9cb08135 100644 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/commands/Verify.java +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/commands/Verify.java @@ -15,39 +15,48 @@ import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.text.DateFormat; +import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.*; -@CommandLine.Command(name = "verify") +@CommandLine.Command(name = "verify", description = "Verify a detached signature.\nThe signed data is being read from standard input.") public class Verify implements Runnable { - @CommandLine.Parameters(index = "0", description = "The detached signature") + @CommandLine.Parameters(index = "0", description = "Detached signature") File signature; - @CommandLine.Parameters(index = "1..*") - File[] certs; + @CommandLine.Parameters(index = "1..*", arity = "1..*", description = "Public key certificates") + File[] certificates; - TimeZone tz = TimeZone.getTimeZone("UTC"); - DateFormat df = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm'Z'"); + @CommandLine.Option(names = {"--not-before"}, description = "ISO-8601 formatted UTC date (eg. '2020-11-23T16:35Z)\n" + + "Reject signatures with a creation date not in range.\n" + + "Defaults to beginning of time (\"-\").") + String notBefore = "-"; + + @CommandLine.Option(names = {"--not-after"}, description = "ISO-8601 formatted UTC date (eg. '2020-11-23T16:35Z)\n" + + "Reject signatures with a creation date not in range.\n" + + "Defaults to current system time (\"now\").\n" + + "Accepts special value \"-\" for end of time.") + String notAfter = "now"; + + private final TimeZone tz = TimeZone.getTimeZone("UTC"); + private final DateFormat df = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm'Z'"); + + private final Date beginningOfTime = new Date(0); + private final Date endOfTime = new Date(8640000000000000L); @Override public void run() { df.setTimeZone(tz); + Date notBeforeDate = parseNotBefore(); + Date notAfterDate = parseNotAfter(); - if (certs.length == 0) { + Map publicKeys = readCertificatesFromFiles(); + if (publicKeys.isEmpty()) { System.out.println("No certificates supplied."); System.exit(19); } - Map publicKeys = new HashMap<>(); - for (File cert : certs) { - try(FileInputStream in = new FileInputStream(cert)) { - publicKeys.put(cert, PGPainless.readKeyRing().publicKeyRing(in)); - } catch (IOException e) { - e.printStackTrace(); - } - } - try(FileInputStream sigIn = new FileInputStream(signature)) { DecryptionStream verifier = PGPainless.decryptAndOrVerify() .onInputStream(System.in) @@ -63,28 +72,73 @@ public class Verify implements Runnable { OpenPgpMetadata metadata = verifier.getResult(); - for (OpenPgpV4Fingerprint sigKeyFp : metadata.getVerifiedSignatures().keySet()) { - PGPSignature signature = metadata.getVerifiedSignatures().get(sigKeyFp); - for (File file : certs) { - // Search signing key ring - PGPPublicKeyRing publicKeyRing = publicKeys.get(file); - if (publicKeyRing.getPublicKey(sigKeyFp.getKeyId()) == null) { - continue; - } - - String utcSigDate = df.format(signature.getCreationTime()); - OpenPgpV4Fingerprint primaryKeyFp = new OpenPgpV4Fingerprint(publicKeyRing); - System.out.println(utcSigDate + " " + sigKeyFp.toString() + " " + primaryKeyFp.toString() + - " signed by " + file.getName()); + Map signaturesInTimeRange = new HashMap<>(); + for (OpenPgpV4Fingerprint fingerprint : metadata.getVerifiedSignatures().keySet()) { + PGPSignature signature = metadata.getVerifiedSignatures().get(fingerprint); + Date creationTime = signature.getCreationTime(); + if (!creationTime.before(notBeforeDate) && !creationTime.after(notAfterDate)) { + signaturesInTimeRange.put(fingerprint, signature); } } - if (metadata.getVerifiedSignatures().isEmpty()) { + if (signaturesInTimeRange.isEmpty()) { System.out.println("Signature validation failed."); System.exit(3); } + + printValidSignatures(signaturesInTimeRange, publicKeys); } catch (IOException | PGPException e) { e.printStackTrace(); } } + + private void printValidSignatures(Map validSignatures, Map publicKeys) { + for (OpenPgpV4Fingerprint sigKeyFp : validSignatures.keySet()) { + PGPSignature signature = validSignatures.get(sigKeyFp); + for (File file : publicKeys.keySet()) { + // Search signing key ring + PGPPublicKeyRing publicKeyRing = publicKeys.get(file); + if (publicKeyRing.getPublicKey(sigKeyFp.getKeyId()) == null) { + continue; + } + + String utcSigDate = df.format(signature.getCreationTime()); + OpenPgpV4Fingerprint primaryKeyFp = new OpenPgpV4Fingerprint(publicKeyRing); + System.out.println(utcSigDate + " " + sigKeyFp.toString() + " " + primaryKeyFp.toString() + + " signed by " + file.getName()); + } + } + } + + private Map readCertificatesFromFiles() { + Map publicKeys = new HashMap<>(); + for (File cert : certificates) { + try(FileInputStream in = new FileInputStream(cert)) { + publicKeys.put(cert, PGPainless.readKeyRing().publicKeyRing(in)); + } catch (IOException e) { + e.printStackTrace(); + } + } + return publicKeys; + } + + private Date parseNotAfter() { + try { + return notAfter.equals("now") ? new Date() : notAfter.equals("-") ? endOfTime : df.parse(notAfter); + } catch (ParseException e) { + System.out.println("Invalid date string supplied as value of --not-after."); + System.exit(1); + return null; + } + } + + private Date parseNotBefore() { + try { + return notBefore.equals("now") ? new Date() : notBefore.equals("-") ? beginningOfTime: df.parse(notBefore); + } catch (ParseException e) { + System.out.println("Invalid date string supplied as value of --not-before."); + System.exit(1); + return null; + } + } } 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 index 25a646b2..772dbf88 100644 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/commands/Version.java +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/commands/Version.java @@ -2,7 +2,7 @@ package org.pgpainless.sop.commands; import picocli.CommandLine; -@CommandLine.Command(name = "version") +@CommandLine.Command(name = "version", description = "Display version information about the tool") public class Version implements Runnable { @Override From fe03197f3e6b0c0758aab7f0dc74cf263315e3f5 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 16 Dec 2020 18:45:13 +0100 Subject: [PATCH 07/25] Use NullOutputStream in verifier to safe resources --- .../org/pgpainless/sop/commands/Verify.java | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/commands/Verify.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/commands/Verify.java index 9cb08135..0ec64eae 100644 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/commands/Verify.java +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/commands/Verify.java @@ -10,14 +10,18 @@ import org.pgpainless.decryption_verification.OpenPgpMetadata; import org.pgpainless.key.OpenPgpV4Fingerprint; import picocli.CommandLine; -import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.IOException; +import java.io.OutputStream; import java.text.DateFormat; import java.text.ParseException; import java.text.SimpleDateFormat; -import java.util.*; +import java.util.Date; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.TimeZone; @CommandLine.Command(name = "verify", description = "Verify a detached signature.\nThe signed data is being read from standard input.") public class Verify implements Runnable { @@ -66,7 +70,7 @@ public class Verify implements Runnable { .ignoreMissingPublicKeys() .build(); - ByteArrayOutputStream out = new ByteArrayOutputStream(); + OutputStream out = new NullOutputStream(); Streams.pipeAll(verifier, out); verifier.close(); @@ -141,4 +145,12 @@ public class Verify implements Runnable { return null; } } + + private static class NullOutputStream extends OutputStream { + + @Override + public void write(int b) throws IOException { + // Nope + } + } } From ffd46b6d5e2c210fd5b608a4f8ad4a83c3002c5f Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 16 Dec 2020 20:08:38 +0100 Subject: [PATCH 08/25] SOP: Do not apply animalsniffer to pgpainless-sop --- build.gradle | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/build.gradle b/build.gradle index 53caf778..7d38069d 100644 --- a/build.gradle +++ b/build.gradle @@ -28,13 +28,15 @@ allprojects { apply plugin: 'jacoco' apply plugin: 'checkstyle' - // animalsniffer - apply plugin: 'ru.vyarus.animalsniffer' - dependencies { - signature "net.sf.androidscents.signature:android-api-level-${pgpainlessMinAndroidSdk}:2.3.1_r2@signature" - } - animalsniffer { - sourceSets = [sourceSets.main] + if (!it.name.equals('pgpainless-sop')) { + // animalsniffer + apply plugin: 'ru.vyarus.animalsniffer' + dependencies { + signature "net.sf.androidscents.signature:android-api-level-${pgpainlessMinAndroidSdk}:2.3.1_r2@signature" + } + animalsniffer { + sourceSets = [sourceSets.main] + } } // checkstyle From 31cfbaa4b2d58513b6154b47bb151e73abbde662 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 16 Dec 2020 20:09:01 +0100 Subject: [PATCH 09/25] Fix various checkstyle issues --- .../org/pgpainless/sop/PGPainlessCLI.java | 48 +++++---- .../main/java/org/pgpainless/sop/Print.java | 27 +++++ .../org/pgpainless/sop/commands/Armor.java | 66 ++++++++++++ .../org/pgpainless/sop/commands/Dearmor.java | 39 +++++++ .../pgpainless/sop/commands/ExtractCert.java | 24 ++++- .../pgpainless/sop/commands/GenerateKey.java | 25 ++++- .../org/pgpainless/sop/commands/Sign.java | 33 ++++-- .../org/pgpainless/sop/commands/Verify.java | 102 ++++++++++++------ .../org/pgpainless/sop/commands/Version.java | 19 +++- .../pgpainless/sop/commands/package-info.java | 19 ++++ .../java/org/pgpainless/sop/package-info.java | 22 ++++ 11 files changed, 350 insertions(+), 74 deletions(-) create mode 100644 pgpainless-sop/src/main/java/org/pgpainless/sop/commands/Armor.java create mode 100644 pgpainless-sop/src/main/java/org/pgpainless/sop/commands/Dearmor.java create mode 100644 pgpainless-sop/src/main/java/org/pgpainless/sop/commands/package-info.java create mode 100644 pgpainless-sop/src/main/java/org/pgpainless/sop/package-info.java diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/PGPainlessCLI.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/PGPainlessCLI.java index dcab8773..7dae6552 100644 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/PGPainlessCLI.java +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/PGPainlessCLI.java @@ -1,6 +1,27 @@ +/* + * Copyright 2020 Paul Schaub. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package org.pgpainless.sop; -import org.pgpainless.sop.commands.*; +import org.pgpainless.sop.commands.Armor; +import org.pgpainless.sop.commands.Dearmor; +import org.pgpainless.sop.commands.ExtractCert; +import org.pgpainless.sop.commands.GenerateKey; +import org.pgpainless.sop.commands.Sign; +import org.pgpainless.sop.commands.Verify; +import org.pgpainless.sop.commands.Version; import picocli.CommandLine; @CommandLine.Command( @@ -9,36 +30,17 @@ import picocli.CommandLine; GenerateKey.class, ExtractCert.class, Sign.class, - Verify.class + Verify.class, + Armor.class, + Dearmor.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 index 2dee1679..0fcb5220 100644 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/Print.java +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/Print.java @@ -1,3 +1,18 @@ +/* + * Copyright 2020 Paul Schaub. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package org.pgpainless.sop; import java.io.IOException; @@ -13,4 +28,16 @@ public class Print { return new String(bytes, "UTF-8"); } } + + public static void print_ln(String msg) { + // CHECKSTYLE:OFF + System.out.println(msg); + // CHECKSTYLE:ON + } + + public static void err_ln(String msg) { + // CHECKSTYLE:OFF + System.err.println(msg); + // CHECKSTYLE:ON + } } diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/commands/Armor.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/commands/Armor.java new file mode 100644 index 00000000..7f028401 --- /dev/null +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/commands/Armor.java @@ -0,0 +1,66 @@ +/* + * Copyright 2020 Paul Schaub. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.pgpainless.sop.commands; + +import org.bouncycastle.bcpg.ArmoredOutputStream; +import org.bouncycastle.util.io.Streams; +import picocli.CommandLine; + +import java.io.IOException; +import java.io.PushbackInputStream; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; + +import static org.pgpainless.sop.Print.err_ln; + +@CommandLine.Command(name = "armor", description = "Add ASCII Armor") +public class Armor implements Runnable { + + private static final byte[] BEGIN_ARMOR = "-----BEGIN PGP".getBytes(StandardCharsets.UTF_8); + + private enum Label { + auto, + sig, + key, + cert, + message + } + + @CommandLine.Option(names = {"--label"}, description = "Label to be used in the header and tail of the armoring.", paramLabel = "{auto|sig|key|cert|message}") + Label label; + + @CommandLine.Option(names = {"--allow-nested"}, description = "Allow additional armoring of already armored input") + boolean allowNested = false; + + @Override + public void run() { + + try (PushbackInputStream pbIn = new PushbackInputStream(System.in); ArmoredOutputStream armoredOutputStream = new ArmoredOutputStream(System.out)) { + byte[] start = new byte[14]; + int read = pbIn.read(start); + pbIn.unread(read); + if (Arrays.equals(BEGIN_ARMOR, start) && !allowNested) { + Streams.pipeAll(pbIn, System.out); + } else { + Streams.pipeAll(pbIn, armoredOutputStream); + } + } catch (IOException e) { + err_ln("Input data cannot be ASCII armored."); + err_ln(e.getMessage()); + System.exit(1); + } + } +} diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/commands/Dearmor.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/commands/Dearmor.java new file mode 100644 index 00000000..9db81bc0 --- /dev/null +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/commands/Dearmor.java @@ -0,0 +1,39 @@ +/* + * Copyright 2020 Paul Schaub. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.pgpainless.sop.commands; + +import org.bouncycastle.bcpg.ArmoredInputStream; +import org.bouncycastle.util.io.Streams; +import picocli.CommandLine; + +import java.io.IOException; + +import static org.pgpainless.sop.Print.err_ln; + +@CommandLine.Command(name = "dearmor", description = "Remove ASCII Armor") +public class Dearmor implements Runnable { + + @Override + public void run() { + try (ArmoredInputStream in = new ArmoredInputStream(System.in, true)) { + Streams.pipeAll(in, System.out); + } catch (IOException e) { + err_ln("Data cannot be dearmored."); + err_ln(e.getMessage()); + System.exit(1); + } + } +} 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 index e4b72484..60687ef8 100644 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/commands/ExtractCert.java +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/commands/ExtractCert.java @@ -1,3 +1,18 @@ +/* + * Copyright 2020 Paul Schaub. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package org.pgpainless.sop.commands; import java.io.IOException; @@ -10,6 +25,9 @@ import org.pgpainless.sop.Print; import org.pgpainless.util.BCUtil; import picocli.CommandLine; +import static org.pgpainless.sop.Print.err_ln; +import static org.pgpainless.sop.Print.print_ln; + @CommandLine.Command(name = "extract-cert") public class ExtractCert implements Runnable { @@ -25,10 +43,10 @@ public class ExtractCert implements Runnable { PGPSecretKeyRing secretKeys = PGPainless.readKeyRing().secretKeyRing(System.in); PGPPublicKeyRing publicKeys = BCUtil.publicKeyRingFromSecretKeyRing(secretKeys); - System.out.println(Print.toString(publicKeys.getEncoded(), !noArmor)); + print_ln(Print.toString(publicKeys.getEncoded(), !noArmor)); } catch (IOException | PGPException e) { - System.err.println("Error extracting certificate from keys;"); - System.err.println(e.getMessage()); + err_ln("Error extracting certificate from keys;"); + err_ln(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 index 745411d7..5398991b 100644 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/commands/GenerateKey.java +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/commands/GenerateKey.java @@ -1,3 +1,18 @@ +/* + * Copyright 2020 Paul Schaub. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package org.pgpainless.sop.commands; import java.io.IOException; @@ -8,9 +23,11 @@ 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; +import static org.pgpainless.sop.Print.err_ln; +import static org.pgpainless.sop.Print.print_ln; + @CommandLine.Command(name = "generate-key") public class GenerateKey implements Runnable { @@ -28,11 +45,11 @@ public class GenerateKey implements Runnable { try { PGPSecretKeyRing secretKeys = PGPainless.generateKeyRing().simpleEcKeyRing(userId); - System.out.println(Print.toString(secretKeys.getEncoded(), !noArmor)); + print_ln(Print.toString(secretKeys.getEncoded(), !noArmor)); } catch (InvalidAlgorithmParameterException | NoSuchAlgorithmException | PGPException | IOException e) { - System.err.println("Error creating OpenPGP key:"); - System.err.println(e.getMessage()); + err_ln("Error creating OpenPGP key:"); + err_ln(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 index 5568ced7..40ca52f0 100644 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/commands/Sign.java +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/commands/Sign.java @@ -1,3 +1,18 @@ +/* + * Copyright 2020 Paul Schaub. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package org.pgpainless.sop.commands; import java.io.ByteArrayOutputStream; @@ -16,6 +31,9 @@ import org.pgpainless.key.protection.UnprotectedKeysProtector; import org.pgpainless.sop.Print; import picocli.CommandLine; +import static org.pgpainless.sop.Print.err_ln; +import static org.pgpainless.sop.Print.print_ln; + @CommandLine.Command(name = "sign") public class Sign implements Runnable { @@ -30,7 +48,8 @@ public class Sign implements Runnable { @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.") + @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.", + paramLabel = "{binary|text}") Type type; @CommandLine.Parameters @@ -42,9 +61,8 @@ public class Sign implements Runnable { 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()); - + err_ln("Error reading secret key ring."); + err_ln(e.getMessage()); System.exit(1); return; } @@ -64,11 +82,10 @@ public class Sign implements Runnable { PGPSignature signature = encryptionStream.getResult().getSignatures().iterator().next(); - System.out.println(Print.toString(signature.getEncoded(), !noArmor)); + print_ln(Print.toString(signature.getEncoded(), !noArmor)); } catch (PGPException | IOException e) { - System.err.println("Error signing data."); - System.err.println(e.getMessage()); - + err_ln("Error signing data."); + err_ln(e.getMessage()); System.exit(1); } } diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/commands/Verify.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/commands/Verify.java index 0ec64eae..28bad051 100644 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/commands/Verify.java +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/commands/Verify.java @@ -1,3 +1,18 @@ +/* + * Copyright 2020 Paul Schaub. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package org.pgpainless.sop.commands; import org.bouncycastle.openpgp.PGPException; @@ -12,6 +27,7 @@ import picocli.CommandLine; import java.io.File; import java.io.FileInputStream; +import java.io.FileNotFoundException; import java.io.IOException; import java.io.OutputStream; import java.text.DateFormat; @@ -23,9 +39,22 @@ import java.util.HashSet; import java.util.Map; import java.util.TimeZone; +import static org.pgpainless.sop.Print.err_ln; +import static org.pgpainless.sop.Print.print_ln; + @CommandLine.Command(name = "verify", description = "Verify a detached signature.\nThe signed data is being read from standard input.") public class Verify implements Runnable { + private static final TimeZone tz = TimeZone.getTimeZone("UTC"); + private static final DateFormat df = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm'Z'"); + + private static final Date beginningOfTime = new Date(0); + private static final Date endOfTime = new Date(8640000000000000L); + + static { + df.setTimeZone(tz); + } + @CommandLine.Parameters(index = "0", description = "Detached signature") File signature; @@ -35,33 +64,27 @@ public class Verify implements Runnable { @CommandLine.Option(names = {"--not-before"}, description = "ISO-8601 formatted UTC date (eg. '2020-11-23T16:35Z)\n" + "Reject signatures with a creation date not in range.\n" + "Defaults to beginning of time (\"-\").") - String notBefore = "-"; + String notBefore = "-"; @CommandLine.Option(names = {"--not-after"}, description = "ISO-8601 formatted UTC date (eg. '2020-11-23T16:35Z)\n" + "Reject signatures with a creation date not in range.\n" + "Defaults to current system time (\"now\").\n" + "Accepts special value \"-\" for end of time.") - String notAfter = "now"; - - private final TimeZone tz = TimeZone.getTimeZone("UTC"); - private final DateFormat df = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm'Z'"); - - private final Date beginningOfTime = new Date(0); - private final Date endOfTime = new Date(8640000000000000L); + String notAfter = "now"; @Override public void run() { - df.setTimeZone(tz); Date notBeforeDate = parseNotBefore(); Date notAfterDate = parseNotAfter(); Map publicKeys = readCertificatesFromFiles(); if (publicKeys.isEmpty()) { - System.out.println("No certificates supplied."); + err_ln("No certificates supplied."); System.exit(19); } - try(FileInputStream sigIn = new FileInputStream(signature)) { + OpenPgpMetadata metadata; + try (FileInputStream sigIn = new FileInputStream(signature)) { DecryptionStream verifier = PGPainless.decryptAndOrVerify() .onInputStream(System.in) .doNotDecrypt() @@ -74,26 +97,34 @@ public class Verify implements Runnable { Streams.pipeAll(verifier, out); verifier.close(); - OpenPgpMetadata metadata = verifier.getResult(); - - Map signaturesInTimeRange = new HashMap<>(); - for (OpenPgpV4Fingerprint fingerprint : metadata.getVerifiedSignatures().keySet()) { - PGPSignature signature = metadata.getVerifiedSignatures().get(fingerprint); - Date creationTime = signature.getCreationTime(); - if (!creationTime.before(notBeforeDate) && !creationTime.after(notAfterDate)) { - signaturesInTimeRange.put(fingerprint, signature); - } - } - - if (signaturesInTimeRange.isEmpty()) { - System.out.println("Signature validation failed."); - System.exit(3); - } - - printValidSignatures(signaturesInTimeRange, publicKeys); + metadata = verifier.getResult(); + } catch (FileNotFoundException e) { + err_ln("Signature file not found:"); + err_ln(e.getMessage()); + System.exit(1); + return; } catch (IOException | PGPException e) { - e.printStackTrace(); + err_ln("Signature validation failed."); + err_ln(e.getMessage()); + System.exit(1); + return; } + + Map signaturesInTimeRange = new HashMap<>(); + for (OpenPgpV4Fingerprint fingerprint : metadata.getVerifiedSignatures().keySet()) { + PGPSignature signature = metadata.getVerifiedSignatures().get(fingerprint); + Date creationTime = signature.getCreationTime(); + if (!creationTime.before(notBeforeDate) && !creationTime.after(notAfterDate)) { + signaturesInTimeRange.put(fingerprint, signature); + } + } + + if (signaturesInTimeRange.isEmpty()) { + err_ln("No valid signatures found."); + System.exit(3); + } + + printValidSignatures(signaturesInTimeRange, publicKeys); } private void printValidSignatures(Map validSignatures, Map publicKeys) { @@ -108,7 +139,7 @@ public class Verify implements Runnable { String utcSigDate = df.format(signature.getCreationTime()); OpenPgpV4Fingerprint primaryKeyFp = new OpenPgpV4Fingerprint(publicKeyRing); - System.out.println(utcSigDate + " " + sigKeyFp.toString() + " " + primaryKeyFp.toString() + + print_ln(utcSigDate + " " + sigKeyFp.toString() + " " + primaryKeyFp.toString() + " signed by " + file.getName()); } } @@ -117,10 +148,11 @@ public class Verify implements Runnable { private Map readCertificatesFromFiles() { Map publicKeys = new HashMap<>(); for (File cert : certificates) { - try(FileInputStream in = new FileInputStream(cert)) { + try (FileInputStream in = new FileInputStream(cert)) { publicKeys.put(cert, PGPainless.readKeyRing().publicKeyRing(in)); } catch (IOException e) { - e.printStackTrace(); + err_ln("Cannot read certificate from file " + cert.getAbsolutePath() + ":"); + err_ln(e.getMessage()); } } return publicKeys; @@ -130,7 +162,7 @@ public class Verify implements Runnable { try { return notAfter.equals("now") ? new Date() : notAfter.equals("-") ? endOfTime : df.parse(notAfter); } catch (ParseException e) { - System.out.println("Invalid date string supplied as value of --not-after."); + err_ln("Invalid date string supplied as value of --not-after."); System.exit(1); return null; } @@ -138,9 +170,9 @@ public class Verify implements Runnable { private Date parseNotBefore() { try { - return notBefore.equals("now") ? new Date() : notBefore.equals("-") ? beginningOfTime: df.parse(notBefore); + return notBefore.equals("now") ? new Date() : notBefore.equals("-") ? beginningOfTime : df.parse(notBefore); } catch (ParseException e) { - System.out.println("Invalid date string supplied as value of --not-before."); + err_ln("Invalid date string supplied as value of --not-before."); System.exit(1); return null; } 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 index 772dbf88..8a5da78b 100644 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/commands/Version.java +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/commands/Version.java @@ -1,12 +1,29 @@ +/* + * Copyright 2020 Paul Schaub. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package org.pgpainless.sop.commands; import picocli.CommandLine; +import static org.pgpainless.sop.Print.print_ln; + @CommandLine.Command(name = "version", description = "Display version information about the tool") public class Version implements Runnable { @Override public void run() { - System.out.println("PGPainless CLI version 0.0.1"); + print_ln("PGPainless CLI version 0.0.1"); } } diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/commands/package-info.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/commands/package-info.java new file mode 100644 index 00000000..633d7afb --- /dev/null +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/commands/package-info.java @@ -0,0 +1,19 @@ +/* + * Copyright 2020 Paul Schaub. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/** + * Subcommands of the PGPainless SOP. + */ +package org.pgpainless.sop.commands; diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/package-info.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/package-info.java new file mode 100644 index 00000000..43afb989 --- /dev/null +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/package-info.java @@ -0,0 +1,22 @@ +/* + * Copyright 2020 Paul Schaub. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/** + * PGPainless SOP implementing a Stateless OpenPGP Command Line Interface. + * @see + * Stateless OpenPGP Command Line Interface + * draft-dkg-openpgp-stateless-cli-01 + */ +package org.pgpainless.sop; From 7d6c0f43963957e492897736120f924a6598f48c Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 22 Dec 2020 22:02:36 +0100 Subject: [PATCH 10/25] Bump picocli to 4.5.2 --- pgpainless-sop/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pgpainless-sop/build.gradle b/pgpainless-sop/build.gradle index ce98b188..f71e1772 100644 --- a/pgpainless-sop/build.gradle +++ b/pgpainless-sop/build.gradle @@ -5,7 +5,7 @@ plugins { dependencies { implementation(project(":pgpainless-core")) - compile 'info.picocli:picocli:3.9.6' + implementation 'info.picocli:picocli:4.5.2' testImplementation "org.junit.jupiter:junit-jupiter-api:$junitVersion" testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:$junitVersion" From 1e3721c4e574bc3ca23912dc8671b3848258e778 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 22 Dec 2020 22:05:47 +0100 Subject: [PATCH 11/25] Improve --[no-]armor flag implementation --- .../org/pgpainless/sop/commands/ExtractCert.java | 11 +++++------ .../org/pgpainless/sop/commands/GenerateKey.java | 11 +++++------ .../java/org/pgpainless/sop/commands/Sign.java | 15 +++++++-------- 3 files changed, 17 insertions(+), 20 deletions(-) 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 index 60687ef8..03c00a3e 100644 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/commands/ExtractCert.java +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/commands/ExtractCert.java @@ -31,11 +31,10 @@ import static org.pgpainless.sop.Print.print_ln; @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; + @CommandLine.Option(names = "--no-armor", + description = "ASCII armor the output", + negatable = true) + boolean armor = true; @Override public void run() { @@ -43,7 +42,7 @@ public class ExtractCert implements Runnable { PGPSecretKeyRing secretKeys = PGPainless.readKeyRing().secretKeyRing(System.in); PGPPublicKeyRing publicKeys = BCUtil.publicKeyRingFromSecretKeyRing(secretKeys); - print_ln(Print.toString(publicKeys.getEncoded(), !noArmor)); + print_ln(Print.toString(publicKeys.getEncoded(), armor)); } catch (IOException | PGPException e) { err_ln("Error extracting certificate from keys;"); err_ln(e.getMessage()); 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 index 5398991b..4ad33cff 100644 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/commands/GenerateKey.java +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/commands/GenerateKey.java @@ -31,11 +31,10 @@ import static org.pgpainless.sop.Print.print_ln; @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"}, description = "Non-armored, binary output") - boolean noArmor = false; + @CommandLine.Option(names = "--no-armor", + description = "ASCII armor the output", + negatable = true) + boolean armor = true; @CommandLine.Parameters(description = "User-ID, eg. \"Alice \"") String userId; @@ -45,7 +44,7 @@ public class GenerateKey implements Runnable { try { PGPSecretKeyRing secretKeys = PGPainless.generateKeyRing().simpleEcKeyRing(userId); - print_ln(Print.toString(secretKeys.getEncoded(), !noArmor)); + print_ln(Print.toString(secretKeys.getEncoded(), armor)); } catch (InvalidAlgorithmParameterException | NoSuchAlgorithmException | PGPException | IOException e) { err_ln("Error creating OpenPGP key:"); 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 index 40ca52f0..928ea9ff 100644 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/commands/Sign.java +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/commands/Sign.java @@ -42,11 +42,10 @@ public class Sign implements Runnable { text } - @CommandLine.Option(names = {"--armor"}, description = "ASCII Armor the output") - boolean armor = false; - - @CommandLine.Option(names = {"--no-armor"}) - boolean noArmor = false; + @CommandLine.Option(names = "--no-armor", + description = "ASCII armor the output", + negatable = true) + boolean armor = true; @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.", paramLabel = "{binary|text}") @@ -74,15 +73,15 @@ public class Sign implements Runnable { .createDetachedSignature() .signWith(new UnprotectedKeysProtector(), secretKeys); - EncryptionBuilderInterface.Armor armor = type == Type.text ? documentType.signCanonicalText() : documentType.signBinaryDocument(); - EncryptionStream encryptionStream = noArmor ? armor.noArmor() : armor.asciiArmor(); + EncryptionBuilderInterface.Armor builder = type == Type.text ? documentType.signCanonicalText() : documentType.signBinaryDocument(); + EncryptionStream encryptionStream = armor ? builder.asciiArmor() : builder.noArmor(); Streams.pipeAll(System.in, encryptionStream); encryptionStream.close(); PGPSignature signature = encryptionStream.getResult().getSignatures().iterator().next(); - print_ln(Print.toString(signature.getEncoded(), !noArmor)); + print_ln(Print.toString(signature.getEncoded(), armor)); } catch (PGPException | IOException e) { err_ln("Error signing data."); err_ln(e.getMessage()); From 7edd6bc86d1b2c9f8050dc05a96dc5e9474c89b3 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 22 Dec 2020 22:07:13 +0100 Subject: [PATCH 12/25] Add descriptions to all tasks --- .../src/main/java/org/pgpainless/sop/commands/ExtractCert.java | 2 +- .../src/main/java/org/pgpainless/sop/commands/GenerateKey.java | 2 +- .../src/main/java/org/pgpainless/sop/commands/Sign.java | 2 +- .../src/main/java/org/pgpainless/sop/commands/Verify.java | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) 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 index 03c00a3e..47ae21ce 100644 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/commands/ExtractCert.java +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/commands/ExtractCert.java @@ -28,7 +28,7 @@ import picocli.CommandLine; import static org.pgpainless.sop.Print.err_ln; import static org.pgpainless.sop.Print.print_ln; -@CommandLine.Command(name = "extract-cert") +@CommandLine.Command(name = "extract-cert", description = "Extract a public key certificate from a secret key") public class ExtractCert implements Runnable { @CommandLine.Option(names = "--no-armor", 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 index 4ad33cff..66dae486 100644 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/commands/GenerateKey.java +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/commands/GenerateKey.java @@ -28,7 +28,7 @@ import picocli.CommandLine; import static org.pgpainless.sop.Print.err_ln; import static org.pgpainless.sop.Print.print_ln; -@CommandLine.Command(name = "generate-key") +@CommandLine.Command(name = "generate-key", description = "Generate a secret key") public class GenerateKey implements Runnable { @CommandLine.Option(names = "--no-armor", 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 index 928ea9ff..ce4dd218 100644 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/commands/Sign.java +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/commands/Sign.java @@ -34,7 +34,7 @@ import picocli.CommandLine; import static org.pgpainless.sop.Print.err_ln; import static org.pgpainless.sop.Print.print_ln; -@CommandLine.Command(name = "sign") +@CommandLine.Command(name = "sign", description = "Create a detached signature") public class Sign implements Runnable { public enum Type { diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/commands/Verify.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/commands/Verify.java index 28bad051..20603119 100644 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/commands/Verify.java +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/commands/Verify.java @@ -42,7 +42,7 @@ import java.util.TimeZone; import static org.pgpainless.sop.Print.err_ln; import static org.pgpainless.sop.Print.print_ln; -@CommandLine.Command(name = "verify", description = "Verify a detached signature.\nThe signed data is being read from standard input.") +@CommandLine.Command(name = "verify", description = "Verify a detached signature") public class Verify implements Runnable { private static final TimeZone tz = TimeZone.getTimeZone("UTC"); From b733b94b9e1330fd7e1f83ac4d9939749c09be2b Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 22 Dec 2020 22:08:38 +0100 Subject: [PATCH 13/25] Allow signing with multiple secret keys --- .../org/pgpainless/sop/commands/Sign.java | 38 +++++++++++-------- 1 file changed, 22 insertions(+), 16 deletions(-) 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 index ce4dd218..f83b5447 100644 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/commands/Sign.java +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/commands/Sign.java @@ -15,11 +15,6 @@ */ 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; @@ -31,6 +26,11 @@ import org.pgpainless.key.protection.UnprotectedKeysProtector; import org.pgpainless.sop.Print; import picocli.CommandLine; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; + import static org.pgpainless.sop.Print.err_ln; import static org.pgpainless.sop.Print.print_ln; @@ -48,22 +48,28 @@ public class Sign implements Runnable { boolean armor = true; @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.", - paramLabel = "{binary|text}") + paramLabel = "{binary|text}") Type type; - @CommandLine.Parameters - File secretKeyFile; + @CommandLine.Parameters(description = "Secret keys used for signing", + paramLabel = "KEY", + arity = "1..*") + File[] secretKeyFile; @Override public void run() { - PGPSecretKeyRing secretKeys; - try { - secretKeys = PGPainless.readKeyRing().secretKeyRing(new FileInputStream(secretKeyFile)); - } catch (IOException | PGPException e) { - err_ln("Error reading secret key ring."); - err_ln(e.getMessage()); - System.exit(1); - return; + PGPSecretKeyRing[] secretKeys = new PGPSecretKeyRing[secretKeyFile.length]; + for (int i = 0, secretKeyFileLength = secretKeyFile.length; i < secretKeyFileLength; i++) { + File file = secretKeyFile[i]; + try { + PGPSecretKeyRing secretKey = PGPainless.readKeyRing().secretKeyRing(new FileInputStream(file)); + secretKeys[i] = secretKey; + } catch (IOException | PGPException e) { + err_ln("Error reading secret key ring " + file.getName()); + err_ln(e.getMessage()); + System.exit(1); + return; + } } try { ByteArrayOutputStream out = new ByteArrayOutputStream(); From 5c11b252121ae95ec6d7a94bdf847159977e823d Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 22 Dec 2020 22:09:22 +0100 Subject: [PATCH 14/25] Verify: set arity of boundary options to 0..1 --- .../src/main/java/org/pgpainless/sop/commands/Verify.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/commands/Verify.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/commands/Verify.java index 20603119..22c43586 100644 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/commands/Verify.java +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/commands/Verify.java @@ -63,13 +63,15 @@ public class Verify implements Runnable { @CommandLine.Option(names = {"--not-before"}, description = "ISO-8601 formatted UTC date (eg. '2020-11-23T16:35Z)\n" + "Reject signatures with a creation date not in range.\n" + - "Defaults to beginning of time (\"-\").") + "Defaults to beginning of time (\"-\").", + arity = "0..1") String notBefore = "-"; @CommandLine.Option(names = {"--not-after"}, description = "ISO-8601 formatted UTC date (eg. '2020-11-23T16:35Z)\n" + "Reject signatures with a creation date not in range.\n" + "Defaults to current system time (\"now\").\n" + - "Accepts special value \"-\" for end of time.") + "Accepts special value \"-\" for end of time.", + arity = "0..1") String notAfter = "now"; @Override From 6382ffb5ae622dcab1eee2cde22d93fcf0739785 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 22 Dec 2020 22:09:42 +0100 Subject: [PATCH 15/25] First implementation of encrypt command --- .../org/pgpainless/sop/PGPainlessCLI.java | 12 +- .../org/pgpainless/sop/commands/Decrypt.java | 12 ++ .../org/pgpainless/sop/commands/Encrypt.java | 157 ++++++++++++++++++ 3 files changed, 177 insertions(+), 4 deletions(-) create mode 100644 pgpainless-sop/src/main/java/org/pgpainless/sop/commands/Decrypt.java create mode 100644 pgpainless-sop/src/main/java/org/pgpainless/sop/commands/Encrypt.java diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/PGPainlessCLI.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/PGPainlessCLI.java index 7dae6552..1cc7cf4e 100644 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/PGPainlessCLI.java +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/PGPainlessCLI.java @@ -17,6 +17,8 @@ package org.pgpainless.sop; import org.pgpainless.sop.commands.Armor; import org.pgpainless.sop.commands.Dearmor; +import org.pgpainless.sop.commands.Decrypt; +import org.pgpainless.sop.commands.Encrypt; import org.pgpainless.sop.commands.ExtractCert; import org.pgpainless.sop.commands.GenerateKey; import org.pgpainless.sop.commands.Sign; @@ -26,13 +28,15 @@ import picocli.CommandLine; @CommandLine.Command( subcommands = { - Version.class, - GenerateKey.class, + Armor.class, + Dearmor.class, + Decrypt.class, + Encrypt.class, ExtractCert.class, + GenerateKey.class, Sign.class, Verify.class, - Armor.class, - Dearmor.class + Version.class } ) public class PGPainlessCLI implements Runnable { diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/commands/Decrypt.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/commands/Decrypt.java new file mode 100644 index 00000000..36c4c3c8 --- /dev/null +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/commands/Decrypt.java @@ -0,0 +1,12 @@ +package org.pgpainless.sop.commands; + +import picocli.CommandLine; + +@CommandLine.Command(name = "decrypt", description = "Decrypt a message") +public class Decrypt implements Runnable { + + @Override + public void run() { + + } +} diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/commands/Encrypt.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/commands/Encrypt.java new file mode 100644 index 00000000..07475bf1 --- /dev/null +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/commands/Encrypt.java @@ -0,0 +1,157 @@ +package org.pgpainless.sop.commands; + +import org.bouncycastle.bcpg.sig.KeyFlags; +import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.PGPPublicKeyRing; +import org.bouncycastle.openpgp.PGPSecretKey; +import org.bouncycastle.openpgp.PGPSecretKeyRing; +import org.bouncycastle.openpgp.PGPSignature; +import org.bouncycastle.openpgp.operator.PBESecretKeyDecryptor; +import org.bouncycastle.openpgp.operator.PBESecretKeyEncryptor; +import org.bouncycastle.util.io.Streams; +import org.pgpainless.PGPainless; +import org.pgpainless.algorithm.KeyFlag; +import org.pgpainless.algorithm.SymmetricKeyAlgorithm; +import org.pgpainless.encryption_signing.EncryptionBuilderInterface; +import org.pgpainless.encryption_signing.EncryptionStream; +import org.pgpainless.key.OpenPgpV4Fingerprint; +import org.pgpainless.key.protection.KeyRingProtectionSettings; +import org.pgpainless.key.protection.PassphraseMapKeyRingProtector; +import org.pgpainless.key.protection.SecretKeyRingProtector; +import org.pgpainless.key.protection.passphrase_provider.SecretKeyPassphraseProvider; +import org.pgpainless.util.Passphrase; +import picocli.CommandLine; + +import javax.annotation.Nullable; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.util.HashMap; +import java.util.Map; +import java.util.Scanner; + +import static org.pgpainless.sop.Print.err_ln; +import static org.pgpainless.sop.Print.print_ln; + +@CommandLine.Command(name = "encrypt", description = "Encrypt a message") +public class Encrypt implements Runnable { + + public enum Type { + binary, + text, + mime + } + + @CommandLine.Option(names = "--no-armor", + description = "ASCII armor the output", + negatable = true) + boolean armor = true; + + @CommandLine.Option(names = {"--as"}, + description = "Type of the input data. Defaults to 'binary'", + paramLabel = "{binary|text|mime}") + Type type; + + @CommandLine.Option(names = "--with-password", + description = "Encrypt the message with a password", + paramLabel = "PASSWORD") + String[] withPassword = new String[0]; + + @CommandLine.Option(names = "--sign-with", + description = "Sign the output with a private key", + paramLabel = "KEY") + File[] signWith = new File[0]; + + @CommandLine.Parameters(description = "Certificates the message gets encrypted to", + index = "0..*", + paramLabel = "CERTS") + File[] certs = new File[0]; + + @Override + public void run() { + if (certs.length == 0 && withPassword.length == 0) { + err_ln("Please either provide --with-password or at least one CERT"); + System.exit(19); + } + + PGPPublicKeyRing[] publicKeys = new PGPPublicKeyRing[certs.length]; + for (int i = 0 ; i < certs.length; i++) { + try (InputStream fileIn = new FileInputStream(certs[i])) { + PGPPublicKeyRing publicKey = PGPainless.readKeyRing().publicKeyRing(fileIn); + publicKeys[i] = publicKey; + } catch (IOException e) { + err_ln("Cannot read certificate " + certs[i].getName()); + err_ln(e.getMessage()); + System.exit(1); + } + } + PGPSecretKeyRing[] secretKeys = new PGPSecretKeyRing[signWith.length]; + for (int i = 0; i < signWith.length; i++) { + try(FileInputStream fileIn = new FileInputStream(signWith[i])) { + PGPSecretKeyRing secretKey = PGPainless.readKeyRing().secretKeyRing(fileIn); + secretKeys[i] = secretKey; + } catch (IOException | PGPException e) { + err_ln("Cannot read secret key from file " + signWith[i].getName()); + err_ln(e.getMessage()); + System.exit(1); + } + } + + Map passphraseMap = new HashMap<>(); + Scanner scanner = null; + for (PGPSecretKeyRing ring : secretKeys) { + for (PGPSecretKey key : ring) { + // Skip non-signing keys + PGPSignature signature = (PGPSignature) key.getPublicKey().getSignatures().next(); + int flags = signature.getHashedSubPackets().getKeyFlags(); + if (!key.isSigningKey() || !KeyFlag.hasKeyFlag(flags, KeyFlag.SIGN_DATA)) { + // Key cannot sign + continue; + } + + if (key.getKeyEncryptionAlgorithm() == SymmetricKeyAlgorithm.NULL.getAlgorithmId()) { + passphraseMap.put(key.getKeyID(), Passphrase.emptyPassphrase()); + } else { + print_ln("Please provide the passphrase for key " + new OpenPgpV4Fingerprint(key)); + if (scanner == null) { + scanner = new Scanner(System.in); + } + String password = scanner.nextLine(); + Passphrase passphrase = Passphrase.fromPassword(password.trim()); + passphraseMap.put(key.getKeyID(), passphrase); + } + } + } + + EncryptionBuilderInterface.DetachedSign builder = PGPainless.encryptAndOrSign() + .onOutputStream(System.out) + .toRecipients(publicKeys) + .usingSecureAlgorithms(); + EncryptionBuilderInterface.Armor builder_armor; + if (signWith.length != 0) { + EncryptionBuilderInterface.DocumentType documentType = builder.signWith(new PassphraseMapKeyRingProtector(passphraseMap, + KeyRingProtectionSettings.secureDefaultSettings(), null), secretKeys); + if (type == Type.text || type == Type.mime) { + builder_armor = documentType.signCanonicalText(); + } else { + builder_armor = documentType.signBinaryDocument(); + } + } else { + builder_armor = builder.doNotSign(); + } + try { + EncryptionStream encryptionStream = !armor ? builder_armor.noArmor() : builder_armor.asciiArmor(); + + Streams.pipeAll(System.in, encryptionStream); + + encryptionStream.close(); + } catch (IOException | PGPException e) { + err_ln("An error happened."); + err_ln(e.getMessage()); + System.exit(1); + } + } +} From fb687ee5d196010e5ae931ea18a5eae284c2cb57 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 22 Dec 2020 23:07:53 +0100 Subject: [PATCH 16/25] Improve subcommand descriptions --- .../src/main/java/org/pgpainless/sop/commands/Armor.java | 3 ++- .../src/main/java/org/pgpainless/sop/commands/Dearmor.java | 3 ++- .../src/main/java/org/pgpainless/sop/commands/Encrypt.java | 3 ++- .../src/main/java/org/pgpainless/sop/commands/ExtractCert.java | 3 ++- .../src/main/java/org/pgpainless/sop/commands/Sign.java | 3 ++- .../src/main/java/org/pgpainless/sop/commands/Verify.java | 3 ++- 6 files changed, 12 insertions(+), 6 deletions(-) diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/commands/Armor.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/commands/Armor.java index 7f028401..7ddf047c 100644 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/commands/Armor.java +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/commands/Armor.java @@ -26,7 +26,8 @@ import java.util.Arrays; import static org.pgpainless.sop.Print.err_ln; -@CommandLine.Command(name = "armor", description = "Add ASCII Armor") +@CommandLine.Command(name = "armor", + description = "Add ASCII Armor to standard input") public class Armor implements Runnable { private static final byte[] BEGIN_ARMOR = "-----BEGIN PGP".getBytes(StandardCharsets.UTF_8); diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/commands/Dearmor.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/commands/Dearmor.java index 9db81bc0..737049bb 100644 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/commands/Dearmor.java +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/commands/Dearmor.java @@ -23,7 +23,8 @@ import java.io.IOException; import static org.pgpainless.sop.Print.err_ln; -@CommandLine.Command(name = "dearmor", description = "Remove ASCII Armor") +@CommandLine.Command(name = "dearmor", + description = "Remove ASCII Armor from standard input") public class Dearmor implements Runnable { @Override diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/commands/Encrypt.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/commands/Encrypt.java index 07475bf1..ac1a7c89 100644 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/commands/Encrypt.java +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/commands/Encrypt.java @@ -36,7 +36,8 @@ import java.util.Scanner; import static org.pgpainless.sop.Print.err_ln; import static org.pgpainless.sop.Print.print_ln; -@CommandLine.Command(name = "encrypt", description = "Encrypt a message") +@CommandLine.Command(name = "encrypt", + description = "Encrypt a message from standard input") public class Encrypt implements Runnable { public enum Type { 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 index 47ae21ce..6814d630 100644 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/commands/ExtractCert.java +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/commands/ExtractCert.java @@ -28,7 +28,8 @@ import picocli.CommandLine; import static org.pgpainless.sop.Print.err_ln; import static org.pgpainless.sop.Print.print_ln; -@CommandLine.Command(name = "extract-cert", description = "Extract a public key certificate from a secret key") +@CommandLine.Command(name = "extract-cert", + description = "Extract a public key certificate from a secret key from standard input") public class ExtractCert implements Runnable { @CommandLine.Option(names = "--no-armor", 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 index f83b5447..7984e141 100644 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/commands/Sign.java +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/commands/Sign.java @@ -34,7 +34,8 @@ import java.io.IOException; import static org.pgpainless.sop.Print.err_ln; import static org.pgpainless.sop.Print.print_ln; -@CommandLine.Command(name = "sign", description = "Create a detached signature") +@CommandLine.Command(name = "sign", + description = "Create a detached signature on the data from standard input") public class Sign implements Runnable { public enum Type { diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/commands/Verify.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/commands/Verify.java index 22c43586..f8ec16aa 100644 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/commands/Verify.java +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/commands/Verify.java @@ -42,7 +42,8 @@ import java.util.TimeZone; import static org.pgpainless.sop.Print.err_ln; import static org.pgpainless.sop.Print.print_ln; -@CommandLine.Command(name = "verify", description = "Verify a detached signature") +@CommandLine.Command(name = "verify", + description = "Verify a detached signature over the data from standard input") public class Verify implements Runnable { private static final TimeZone tz = TimeZone.getTimeZone("UTC"); From 59e0a53c3f8200b624fe45e914b0d9bc762d24ac Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 22 Dec 2020 23:08:53 +0100 Subject: [PATCH 17/25] Add missing copyright headers --- .../java/org/pgpainless/sop/commands/Decrypt.java | 15 +++++++++++++++ .../java/org/pgpainless/sop/commands/Encrypt.java | 15 +++++++++++++++ 2 files changed, 30 insertions(+) diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/commands/Decrypt.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/commands/Decrypt.java index 36c4c3c8..91a47f2a 100644 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/commands/Decrypt.java +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/commands/Decrypt.java @@ -1,3 +1,18 @@ +/* + * Copyright 2020 Paul Schaub. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package org.pgpainless.sop.commands; import picocli.CommandLine; diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/commands/Encrypt.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/commands/Encrypt.java index ac1a7c89..58f4ccf3 100644 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/commands/Encrypt.java +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/commands/Encrypt.java @@ -1,3 +1,18 @@ +/* + * Copyright 2020 Paul Schaub. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package org.pgpainless.sop.commands; import org.bouncycastle.bcpg.sig.KeyFlags; From ee474232c84d12cde5ae61d31c7b6f6655fa24a6 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 22 Dec 2020 23:09:17 +0100 Subject: [PATCH 18/25] Remove unnecessary imports --- .../main/java/org/pgpainless/sop/commands/Encrypt.java | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/commands/Encrypt.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/commands/Encrypt.java index 58f4ccf3..877bcac5 100644 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/commands/Encrypt.java +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/commands/Encrypt.java @@ -15,14 +15,11 @@ */ package org.pgpainless.sop.commands; -import org.bouncycastle.bcpg.sig.KeyFlags; import org.bouncycastle.openpgp.PGPException; import org.bouncycastle.openpgp.PGPPublicKeyRing; import org.bouncycastle.openpgp.PGPSecretKey; import org.bouncycastle.openpgp.PGPSecretKeyRing; import org.bouncycastle.openpgp.PGPSignature; -import org.bouncycastle.openpgp.operator.PBESecretKeyDecryptor; -import org.bouncycastle.openpgp.operator.PBESecretKeyEncryptor; import org.bouncycastle.util.io.Streams; import org.pgpainless.PGPainless; import org.pgpainless.algorithm.KeyFlag; @@ -32,18 +29,13 @@ import org.pgpainless.encryption_signing.EncryptionStream; import org.pgpainless.key.OpenPgpV4Fingerprint; import org.pgpainless.key.protection.KeyRingProtectionSettings; import org.pgpainless.key.protection.PassphraseMapKeyRingProtector; -import org.pgpainless.key.protection.SecretKeyRingProtector; -import org.pgpainless.key.protection.passphrase_provider.SecretKeyPassphraseProvider; import org.pgpainless.util.Passphrase; import picocli.CommandLine; -import javax.annotation.Nullable; import java.io.File; import java.io.FileInputStream; -import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; -import java.nio.charset.StandardCharsets; import java.util.HashMap; import java.util.Map; import java.util.Scanner; @@ -106,7 +98,7 @@ public class Encrypt implements Runnable { } PGPSecretKeyRing[] secretKeys = new PGPSecretKeyRing[signWith.length]; for (int i = 0; i < signWith.length; i++) { - try(FileInputStream fileIn = new FileInputStream(signWith[i])) { + try (FileInputStream fileIn = new FileInputStream(signWith[i])) { PGPSecretKeyRing secretKey = PGPainless.readKeyRing().secretKeyRing(fileIn); secretKeys[i] = secretKey; } catch (IOException | PGPException e) { From 11b1acf7f66cf07559ccb24d79b5af05a89aaff6 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 22 Dec 2020 23:09:41 +0100 Subject: [PATCH 19/25] Improve verify command descriptions --- .../org/pgpainless/sop/commands/Verify.java | 29 ++++++++++++------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/commands/Verify.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/commands/Verify.java index f8ec16aa..cb66c7a6 100644 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/commands/Verify.java +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/commands/Verify.java @@ -56,23 +56,30 @@ public class Verify implements Runnable { df.setTimeZone(tz); } - @CommandLine.Parameters(index = "0", description = "Detached signature") + @CommandLine.Parameters(index = "0", + description = "Detached signature", + paramLabel = "SIGNATURE") File signature; - @CommandLine.Parameters(index = "1..*", arity = "1..*", description = "Public key certificates") + @CommandLine.Parameters(index = "1..*", + arity = "1..*", + description = "Public key certificates", + paramLabel = "CERT") File[] certificates; - @CommandLine.Option(names = {"--not-before"}, description = "ISO-8601 formatted UTC date (eg. '2020-11-23T16:35Z)\n" + - "Reject signatures with a creation date not in range.\n" + - "Defaults to beginning of time (\"-\").", - arity = "0..1") + @CommandLine.Option(names = {"--not-before"}, + description = "ISO-8601 formatted UTC date (eg. '2020-11-23T16:35Z)\n" + + "Reject signatures with a creation date not in range.\n" + + "Defaults to beginning of time (\"-\").", + paramLabel = "DATE") String notBefore = "-"; - @CommandLine.Option(names = {"--not-after"}, description = "ISO-8601 formatted UTC date (eg. '2020-11-23T16:35Z)\n" + - "Reject signatures with a creation date not in range.\n" + - "Defaults to current system time (\"now\").\n" + - "Accepts special value \"-\" for end of time.", - arity = "0..1") + @CommandLine.Option(names = {"--not-after"}, + description = "ISO-8601 formatted UTC date (eg. '2020-11-23T16:35Z)\n" + + "Reject signatures with a creation date not in range.\n" + + "Defaults to current system time (\"now\").\n" + + "Accepts special value \"-\" for end of time.", + paramLabel = "DATE") String notAfter = "now"; @Override From ce4f98423fa30ebda042adc291abb47a5dffbc08 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 22 Dec 2020 23:10:24 +0100 Subject: [PATCH 20/25] Start implementing decrypt command --- .../org/pgpainless/sop/commands/Decrypt.java | 60 ++++++++++++++++++- 1 file changed, 58 insertions(+), 2 deletions(-) diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/commands/Decrypt.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/commands/Decrypt.java index 91a47f2a..594967e4 100644 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/commands/Decrypt.java +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/commands/Decrypt.java @@ -17,11 +17,67 @@ package org.pgpainless.sop.commands; import picocli.CommandLine; -@CommandLine.Command(name = "decrypt", description = "Decrypt a message") +import java.io.File; + +import static org.pgpainless.sop.Print.err_ln; + +@CommandLine.Command(name = "decrypt", + description = "Decrypt a message from standard input") public class Decrypt implements Runnable { + @CommandLine.Option( + names = {"--session-key-out"}, + description = "Can be used to learn the session key on successful decryption", + paramLabel = "SESSIONKEY") + File sessionKeyOut; + + @CommandLine.Option( + names = {"--with-session-key"}, + description = "Enables decryption of the \"CIPHERTEXT\" using the session key directly against the \"SEIPD\" packet", + paramLabel = "SESSIONKEY") + File[] withSessionKey; + + @CommandLine.Option( + names = {"--with-password"}, + description = "Enables decryption based on any \"SKESK\" packets in the \"CIPHERTEXT\"", + paramLabel = "PASSWORD") + String[] withPassword; + + @CommandLine.Option(names = {"--verify-out"}, + description = "Produces signature verification status to the designated file", + paramLabel = "VERIFICATIONS") + File verifyOut; + + @CommandLine.Option(names = {"--verify-with"}, + description = "Certificates whose signatures would be acceptable for signatures over this message", + paramLabel = "CERT") + File[] certs; + + @CommandLine.Option(names = {"--not-before"}, + description = "ISO-8601 formatted UTC date (eg. '2020-11-23T16:35Z)\n" + + "Reject signatures with a creation date not in range.\n" + + "Defaults to beginning of time (\"-\").", + paramLabel = "DATE") + String notBefore = "-"; + + @CommandLine.Option(names = {"--not-after"}, + description = "ISO-8601 formatted UTC date (eg. '2020-11-23T16:35Z)\n" + + "Reject signatures with a creation date not in range.\n" + + "Defaults to current system time (\"now\").\n" + + "Accepts special value \"-\" for end of time.", + paramLabel = "DATE") + String notAfter = "now"; + + @CommandLine.Parameters(index = "0..*", + description = "Secret keys to attempt decryption with", + paramLabel = "KEY") + File[] keys; + @Override public void run() { - + if (verifyOut == null ^ certs == null) { + err_ln("To enable signature verification, both --verify-out and at least one --verify-with argument must be supplied."); + System.exit(23); + } } } From 7d374f10a723c254533571fa860e8b3a53067b00 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Sat, 26 Dec 2020 19:04:27 +0100 Subject: [PATCH 21/25] Allow encryption and decryption using symmetric passphrases in the main API --- .../main/java/org/pgpainless/PGPainless.java | 6 ++ .../DecryptionBuilder.java | 13 ++- .../DecryptionBuilderInterface.java | 10 ++ .../DecryptionStreamFactory.java | 58 ++++++++--- .../encryption_signing/EncryptionBuilder.java | 83 +++++++++++----- .../EncryptionBuilderInterface.java | 12 +++ .../encryption_signing/EncryptionStream.java | 11 ++- .../LegacySymmetricEncryptionTest.java | 67 +++++++++++++ .../SymmetricEncryptionTest.java | 98 ++++++++++++------- .../org/pgpainless/sop/commands/Encrypt.java | 29 +++--- 10 files changed, 303 insertions(+), 84 deletions(-) create mode 100644 pgpainless-core/src/test/java/org/pgpainless/symmetric_encryption/LegacySymmetricEncryptionTest.java diff --git a/pgpainless-core/src/main/java/org/pgpainless/PGPainless.java b/pgpainless-core/src/main/java/org/pgpainless/PGPainless.java index 2507aef9..3fd90f82 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/PGPainless.java +++ b/pgpainless-core/src/main/java/org/pgpainless/PGPainless.java @@ -116,7 +116,10 @@ public class PGPainless { * * @throws IOException IO is dangerous. * @throws PGPException PGP is brittle. + * @deprecated use {@link #encryptAndOrSign()} instead and provide a passphrase in + * {@link org.pgpainless.encryption_signing.EncryptionBuilderInterface.ToRecipients#forPassphrases(Passphrase...)}. */ + @Deprecated public static byte[] encryptWithPassword(@Nonnull byte[] data, @Nonnull Passphrase password, @Nonnull SymmetricKeyAlgorithm algorithm) throws IOException, PGPException { return SymmetricEncryptorDecryptor.symmetricallyEncrypt(data, password, algorithm, CompressionAlgorithm.UNCOMPRESSED); @@ -131,7 +134,10 @@ public class PGPainless { * @return decrypted data. * @throws IOException IO is dangerous. * @throws PGPException PGP is brittle. + * @deprecated Use {@link #decryptAndOrVerify()} instead and provide the decryption passphrase in + * {@link org.pgpainless.decryption_verification.DecryptionBuilder.DecryptWith#decryptWith(Passphrase)}. */ + @Deprecated public static byte[] decryptWithPassword(@Nonnull byte[] data, @Nonnull Passphrase password) throws IOException, PGPException { return SymmetricEncryptorDecryptor.symmetricallyDecrypt(data, password); } diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionBuilder.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionBuilder.java index 1501c5da..f78e03c4 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionBuilder.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionBuilder.java @@ -37,12 +37,14 @@ import org.bouncycastle.openpgp.operator.KeyFingerPrintCalculator; import org.bouncycastle.openpgp.operator.bc.BcKeyFingerprintCalculator; import org.pgpainless.key.OpenPgpV4Fingerprint; import org.pgpainless.key.protection.SecretKeyRingProtector; +import org.pgpainless.util.Passphrase; public class DecryptionBuilder implements DecryptionBuilderInterface { private InputStream inputStream; private PGPSecretKeyRingCollection decryptionKeys; private SecretKeyRingProtector decryptionKeyDecryptor; + private Passphrase decryptionPassphrase; private List detachedSignatures; private Set verificationKeys = new HashSet<>(); private MissingPublicKeyCallback missingPublicKeyCallback = null; @@ -64,6 +66,15 @@ public class DecryptionBuilder implements DecryptionBuilderInterface { return new VerifyImpl(); } + @Override + public Verify decryptWith(@Nonnull Passphrase passphrase) { + if (passphrase.isEmpty()) { + throw new IllegalArgumentException("Passphrase MUST NOT be empty."); + } + DecryptionBuilder.this.decryptionPassphrase = passphrase; + return new VerifyImpl(); + } + @Override public Verify doNotDecrypt() { DecryptionBuilder.this.decryptionKeys = null; @@ -194,7 +205,7 @@ public class DecryptionBuilder implements DecryptionBuilderInterface { @Override public DecryptionStream build() throws IOException, PGPException { return DecryptionStreamFactory.create(inputStream, - decryptionKeys, decryptionKeyDecryptor, detachedSignatures, verificationKeys, missingPublicKeyCallback); + decryptionKeys, decryptionKeyDecryptor, decryptionPassphrase, detachedSignatures, verificationKeys, missingPublicKeyCallback); } } } diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionBuilderInterface.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionBuilderInterface.java index cc41f5d6..608c5f63 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionBuilderInterface.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionBuilderInterface.java @@ -31,6 +31,7 @@ import org.bouncycastle.openpgp.PGPSignature; import org.pgpainless.key.OpenPgpV4Fingerprint; import org.pgpainless.key.protection.SecretKeyRingProtector; import org.pgpainless.key.protection.UnprotectedKeysProtector; +import org.pgpainless.util.Passphrase; public interface DecryptionBuilderInterface { @@ -67,6 +68,15 @@ public interface DecryptionBuilderInterface { */ Verify decryptWith(@Nonnull SecretKeyRingProtector decryptor, @Nonnull PGPSecretKeyRingCollection secretKeyRings); + /** + * Decrypt the encrypted data using a passphrase. + * Note: The passphrase MUST NOT be empty. + * + * @param passphrase passphrase + * @return api handle + */ + Verify decryptWith(@Nonnull Passphrase passphrase); + Verify doNotDecrypt(); } diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionStreamFactory.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionStreamFactory.java index 784d9bab..130eb537 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionStreamFactory.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionStreamFactory.java @@ -30,12 +30,14 @@ import java.util.logging.Level; import java.util.logging.Logger; import org.bouncycastle.openpgp.PGPCompressedData; +import org.bouncycastle.openpgp.PGPEncryptedData; import org.bouncycastle.openpgp.PGPEncryptedDataList; import org.bouncycastle.openpgp.PGPException; import org.bouncycastle.openpgp.PGPLiteralData; import org.bouncycastle.openpgp.PGPObjectFactory; import org.bouncycastle.openpgp.PGPOnePassSignature; import org.bouncycastle.openpgp.PGPOnePassSignatureList; +import org.bouncycastle.openpgp.PGPPBEEncryptedData; import org.bouncycastle.openpgp.PGPPrivateKey; import org.bouncycastle.openpgp.PGPPublicKey; import org.bouncycastle.openpgp.PGPPublicKeyEncryptedData; @@ -45,15 +47,19 @@ import org.bouncycastle.openpgp.PGPSecretKeyRingCollection; import org.bouncycastle.openpgp.PGPSignature; import org.bouncycastle.openpgp.PGPUtil; import org.bouncycastle.openpgp.operator.KeyFingerPrintCalculator; +import org.bouncycastle.openpgp.operator.PBEDataDecryptorFactory; import org.bouncycastle.openpgp.operator.PGPContentVerifierBuilderProvider; import org.bouncycastle.openpgp.operator.PublicKeyDataDecryptorFactory; import org.bouncycastle.openpgp.operator.bc.BcKeyFingerprintCalculator; +import org.bouncycastle.openpgp.operator.bc.BcPBEDataDecryptorFactory; import org.bouncycastle.openpgp.operator.bc.BcPGPContentVerifierBuilderProvider; +import org.bouncycastle.openpgp.operator.bc.BcPGPDigestCalculatorProvider; import org.bouncycastle.openpgp.operator.bc.BcPublicKeyDataDecryptorFactory; import org.pgpainless.algorithm.CompressionAlgorithm; import org.pgpainless.algorithm.SymmetricKeyAlgorithm; import org.pgpainless.key.OpenPgpV4Fingerprint; import org.pgpainless.key.protection.SecretKeyRingProtector; +import org.pgpainless.util.Passphrase; public final class DecryptionStreamFactory { @@ -62,6 +68,7 @@ public final class DecryptionStreamFactory { private final PGPSecretKeyRingCollection decryptionKeys; private final SecretKeyRingProtector decryptionKeyDecryptor; + private final Passphrase decryptionPassphrase; private final Set verificationKeys = new HashSet<>(); private final MissingPublicKeyCallback missingPublicKeyCallback; @@ -72,10 +79,12 @@ public final class DecryptionStreamFactory { private DecryptionStreamFactory(@Nullable PGPSecretKeyRingCollection decryptionKeys, @Nullable SecretKeyRingProtector decryptor, + @Nullable Passphrase decryptionPassphrase, @Nullable Set verificationKeys, @Nullable MissingPublicKeyCallback missingPublicKeyCallback) { this.decryptionKeys = decryptionKeys; this.decryptionKeyDecryptor = decryptor; + this.decryptionPassphrase = decryptionPassphrase; this.verificationKeys.addAll(verificationKeys != null ? verificationKeys : Collections.emptyList()); this.missingPublicKeyCallback = missingPublicKeyCallback; } @@ -83,13 +92,14 @@ public final class DecryptionStreamFactory { public static DecryptionStream create(@Nonnull InputStream inputStream, @Nullable PGPSecretKeyRingCollection decryptionKeys, @Nullable SecretKeyRingProtector decryptor, + @Nullable Passphrase decryptionPassphrase, @Nullable List detachedSignatures, @Nullable Set verificationKeys, @Nullable MissingPublicKeyCallback missingPublicKeyCallback) throws IOException, PGPException { InputStream pgpInputStream; - DecryptionStreamFactory factory = new DecryptionStreamFactory(decryptionKeys, decryptor, verificationKeys, - missingPublicKeyCallback); + DecryptionStreamFactory factory = new DecryptionStreamFactory(decryptionKeys, decryptor, + decryptionPassphrase, verificationKeys, missingPublicKeyCallback); if (detachedSignatures != null) { pgpInputStream = inputStream; @@ -171,7 +181,7 @@ public final class DecryptionStreamFactory { private InputStream decrypt(@Nonnull PGPEncryptedDataList encryptedDataList) throws PGPException { - Iterator encryptedDataIterator = encryptedDataList.getEncryptedDataObjects(); + Iterator encryptedDataIterator = encryptedDataList.getEncryptedDataObjects(); if (!encryptedDataIterator.hasNext()) { throw new PGPException("Decryption failed - EncryptedDataList has no items"); } @@ -179,19 +189,38 @@ public final class DecryptionStreamFactory { PGPPrivateKey decryptionKey = null; PGPPublicKeyEncryptedData encryptedSessionKey = null; while (encryptedDataIterator.hasNext()) { - PGPPublicKeyEncryptedData encryptedData = (PGPPublicKeyEncryptedData) encryptedDataIterator.next(); - long keyId = encryptedData.getKeyID(); + PGPEncryptedData encryptedData = encryptedDataIterator.next(); - resultBuilder.addRecipientKeyId(keyId); - LOGGER.log(LEVEL, "PGPEncryptedData is encrypted for key " + Long.toHexString(keyId)); + if (encryptedData instanceof PGPPBEEncryptedData) { - PGPSecretKey secretKey = decryptionKeys.getSecretKey(keyId); - if (secretKey != null) { - LOGGER.log(LEVEL, "Found respective secret key " + Long.toHexString(keyId)); - // Watch out! This assignment is possibly done multiple times. - encryptedSessionKey = encryptedData; - decryptionKey = secretKey.extractPrivateKey(decryptionKeyDecryptor.getDecryptor(keyId)); - resultBuilder.setDecryptionFingerprint(new OpenPgpV4Fingerprint(secretKey)); + PGPPBEEncryptedData pbeEncryptedData = (PGPPBEEncryptedData) encryptedData; + if (decryptionPassphrase != null) { + PBEDataDecryptorFactory passphraseDecryptor = new BcPBEDataDecryptorFactory( + decryptionPassphrase.getChars(), new BcPGPDigestCalculatorProvider()); + SymmetricKeyAlgorithm symmetricKeyAlgorithm = SymmetricKeyAlgorithm.fromId( + pbeEncryptedData.getSymmetricAlgorithm(passphraseDecryptor)); + resultBuilder.setSymmetricKeyAlgorithm(symmetricKeyAlgorithm); + resultBuilder.setIntegrityProtected(pbeEncryptedData.isIntegrityProtected()); + return pbeEncryptedData.getDataStream(passphraseDecryptor); + } + + } else if (encryptedData instanceof PGPPublicKeyEncryptedData) { + PGPPublicKeyEncryptedData publicKeyEncryptedData = (PGPPublicKeyEncryptedData) encryptedData; + long keyId = publicKeyEncryptedData.getKeyID(); + + resultBuilder.addRecipientKeyId(keyId); + LOGGER.log(LEVEL, "PGPEncryptedData is encrypted for key " + Long.toHexString(keyId)); + + if (decryptionKeys != null) { + PGPSecretKey secretKey = decryptionKeys.getSecretKey(keyId); + if (secretKey != null) { + LOGGER.log(LEVEL, "Found respective secret key " + Long.toHexString(keyId)); + // Watch out! This assignment is possibly done multiple times. + encryptedSessionKey = publicKeyEncryptedData; + decryptionKey = secretKey.extractPrivateKey(decryptionKeyDecryptor.getDecryptor(keyId)); + resultBuilder.setDecryptionFingerprint(new OpenPgpV4Fingerprint(secretKey)); + } + } } } @@ -200,6 +229,7 @@ public final class DecryptionStreamFactory { } PublicKeyDataDecryptorFactory keyDecryptor = new BcPublicKeyDataDecryptorFactory(decryptionKey); + SymmetricKeyAlgorithm symmetricKeyAlgorithm = SymmetricKeyAlgorithm .fromId(encryptedSessionKey.getSymmetricAlgorithm(keyDecryptor)); diff --git a/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/EncryptionBuilder.java b/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/EncryptionBuilder.java index a1ea9536..b0f6d022 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/EncryptionBuilder.java +++ b/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/EncryptionBuilder.java @@ -17,8 +17,10 @@ package org.pgpainless.encryption_signing; import java.io.IOException; import java.io.OutputStream; +import java.util.ArrayList; import java.util.HashSet; import java.util.Iterator; +import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; @@ -49,11 +51,13 @@ import org.pgpainless.key.selection.key.util.And; import org.pgpainless.key.selection.keyring.PublicKeyRingSelectionStrategy; import org.pgpainless.key.selection.keyring.SecretKeyRingSelectionStrategy; import org.pgpainless.util.MultiMap; +import org.pgpainless.util.Passphrase; public class EncryptionBuilder implements EncryptionBuilderInterface { private OutputStream outputStream; private final Set encryptionKeys = new HashSet<>(); + private final Set encryptionPassphrases = new HashSet<>(); private boolean detachedSignature = false; private SignatureType signatureType = SignatureType.BINARY_DOCUMENT; private final Set signingKeys = new HashSet<>(); @@ -73,16 +77,20 @@ public class EncryptionBuilder implements EncryptionBuilderInterface { @Override public WithAlgorithms toRecipients(@Nonnull PGPPublicKey... keys) { - for (PGPPublicKey k : keys) { - if (encryptionKeySelector().accept(null, k)) { - EncryptionBuilder.this.encryptionKeys.add(k); - } else { - throw new IllegalArgumentException("Key " + k.getKeyID() + " is not a valid encryption key."); + if (keys.length != 0) { + List encryptionKeys = new ArrayList<>(); + for (PGPPublicKey k : keys) { + if (encryptionKeySelector().accept(null, k)) { + encryptionKeys.add(k); + } else { + throw new IllegalArgumentException("Key " + k.getKeyID() + " is not a valid encryption key."); + } } - } - if (EncryptionBuilder.this.encryptionKeys.isEmpty()) { - throw new IllegalStateException("No valid encryption keys found!"); + if (encryptionKeys.isEmpty()) { + throw new IllegalStateException("No valid encryption keys found!"); + } + EncryptionBuilder.this.encryptionKeys.addAll(encryptionKeys); } return new WithAlgorithmsImpl(); @@ -90,16 +98,19 @@ public class EncryptionBuilder implements EncryptionBuilderInterface { @Override public WithAlgorithms toRecipients(@Nonnull PGPPublicKeyRing... keys) { - for (PGPPublicKeyRing ring : keys) { - for (PGPPublicKey k : ring) { - if (encryptionKeySelector().accept(null, k)) { - EncryptionBuilder.this.encryptionKeys.add(k); + if (keys.length != 0) { + List encryptionKeys = new ArrayList<>(); + for (PGPPublicKeyRing ring : keys) { + for (PGPPublicKey k : ring) { + if (encryptionKeySelector().accept(null, k)) { + encryptionKeys.add(k); + } } } - } - - if (EncryptionBuilder.this.encryptionKeys.isEmpty()) { - throw new IllegalStateException("No valid encryption keys found!"); + if (encryptionKeys.isEmpty()) { + throw new IllegalStateException("No valid encryption keys found!"); + } + EncryptionBuilder.this.encryptionKeys.addAll(encryptionKeys); } return new WithAlgorithmsImpl(); @@ -107,18 +118,23 @@ public class EncryptionBuilder implements EncryptionBuilderInterface { @Override public WithAlgorithms toRecipients(@Nonnull PGPPublicKeyRingCollection... keys) { - for (PGPPublicKeyRingCollection collection : keys) { - for (PGPPublicKeyRing ring : collection) { - for (PGPPublicKey k : ring) { - if (encryptionKeySelector().accept(null, k)) { - EncryptionBuilder.this.encryptionKeys.add(k); + if (keys.length != 0) { + List encryptionKeys = new ArrayList<>(); + for (PGPPublicKeyRingCollection collection : keys) { + for (PGPPublicKeyRing ring : collection) { + for (PGPPublicKey k : ring) { + if (encryptionKeySelector().accept(null, k)) { + encryptionKeys.add(k); + } } } } - } - if (EncryptionBuilder.this.encryptionKeys.isEmpty()) { - throw new IllegalStateException("No valid encryption keys found!"); + if (encryptionKeys.isEmpty()) { + throw new IllegalStateException("No valid encryption keys found!"); + } + + EncryptionBuilder.this.encryptionKeys.addAll(encryptionKeys); } return new WithAlgorithmsImpl(); @@ -149,6 +165,19 @@ public class EncryptionBuilder implements EncryptionBuilderInterface { return new WithAlgorithmsImpl(); } + @Override + public WithAlgorithms forPassphrases(Passphrase... passphrases) { + List passphraseList = new ArrayList<>(); + for (Passphrase passphrase : passphrases) { + if (passphrase.isEmpty()) { + throw new IllegalArgumentException("Passphrase must not be empty."); + } + passphraseList.add(passphrase); + } + EncryptionBuilder.this.encryptionPassphrases.addAll(passphraseList); + return new WithAlgorithmsImpl(); + } + @Override public DetachedSign doNotEncrypt() { return new DetachedSignImpl(); @@ -243,6 +272,11 @@ public class EncryptionBuilder implements EncryptionBuilderInterface { return new DetachedSignImpl(); } + + @Override + public ToRecipients and() { + return new ToRecipientsImpl(); + } } class DetachedSignImpl implements DetachedSign { @@ -379,6 +413,7 @@ public class EncryptionBuilder implements EncryptionBuilderInterface { return new EncryptionStream( EncryptionBuilder.this.outputStream, EncryptionBuilder.this.encryptionKeys, + EncryptionBuilder.this.encryptionPassphrases, EncryptionBuilder.this.detachedSignature, signatureType, privateKeys, diff --git a/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/EncryptionBuilderInterface.java b/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/EncryptionBuilderInterface.java index 22645d1d..5d1b4281 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/EncryptionBuilderInterface.java +++ b/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/EncryptionBuilderInterface.java @@ -36,6 +36,7 @@ import org.pgpainless.key.protection.UnprotectedKeysProtector; import org.pgpainless.key.selection.keyring.PublicKeyRingSelectionStrategy; import org.pgpainless.key.selection.keyring.SecretKeyRingSelectionStrategy; import org.pgpainless.util.MultiMap; +import org.pgpainless.util.Passphrase; public interface EncryptionBuilderInterface { @@ -85,6 +86,15 @@ public interface EncryptionBuilderInterface { WithAlgorithms toRecipients(@Nonnull PublicKeyRingSelectionStrategy selectionStrategy, @Nonnull MultiMap keys); + /** + * Encrypt to one or more symmetric passphrases. + * Note that the passphrases MUST NOT be empty. + * + * @param passphrases passphrase + * @return api handle + */ + WithAlgorithms forPassphrases(Passphrase... passphrases); + /** * Instruct the {@link EncryptionStream} to not encrypt any data. * @@ -150,6 +160,8 @@ public interface EncryptionBuilderInterface { */ DetachedSign usingSecureAlgorithms(); + ToRecipients and(); + } interface DetachedSign extends SignWith { diff --git a/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/EncryptionStream.java b/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/EncryptionStream.java index 08f35959..39a10e0f 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/EncryptionStream.java +++ b/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/EncryptionStream.java @@ -37,6 +37,7 @@ import org.bouncycastle.openpgp.PGPPrivateKey; import org.bouncycastle.openpgp.PGPPublicKey; import org.bouncycastle.openpgp.PGPSignature; import org.bouncycastle.openpgp.PGPSignatureGenerator; +import org.bouncycastle.openpgp.operator.bc.BcPBEKeyEncryptionMethodGenerator; import org.bouncycastle.openpgp.operator.bc.BcPGPContentSignerBuilder; import org.bouncycastle.openpgp.operator.bc.BcPGPDataEncryptorBuilder; import org.bouncycastle.openpgp.operator.bc.BcPublicKeyKeyEncryptionMethodGenerator; @@ -47,6 +48,7 @@ import org.pgpainless.algorithm.SymmetricKeyAlgorithm; import org.pgpainless.decryption_verification.DetachedSignature; import org.pgpainless.decryption_verification.OpenPgpMetadata; import org.pgpainless.key.OpenPgpV4Fingerprint; +import org.pgpainless.util.Passphrase; /** * This class is based upon Jens Neuhalfen's Bouncy-GPG PGPEncryptingStream. @@ -63,6 +65,7 @@ public final class EncryptionStream extends OutputStream { private final HashAlgorithm hashAlgorithm; private final CompressionAlgorithm compressionAlgorithm; private final Set encryptionKeys; + private final Set encryptionPassphrases; private final boolean detachedSignature; private final SignatureType signatureType; private final Map signingKeys; @@ -86,6 +89,7 @@ public final class EncryptionStream extends OutputStream { EncryptionStream(@Nonnull OutputStream targetOutputStream, @Nonnull Set encryptionKeys, + @Nonnull Set encryptionPassphrases, boolean detachedSignature, SignatureType signatureType, @Nonnull Map signingKeys, @@ -99,6 +103,7 @@ public final class EncryptionStream extends OutputStream { this.hashAlgorithm = hashAlgorithm; this.compressionAlgorithm = compressionAlgorithm; this.encryptionKeys = Collections.unmodifiableSet(encryptionKeys); + this.encryptionPassphrases = Collections.unmodifiableSet(encryptionPassphrases); this.detachedSignature = detachedSignature; this.signatureType = signatureType; this.signingKeys = Collections.unmodifiableMap(signingKeys); @@ -126,7 +131,7 @@ public final class EncryptionStream extends OutputStream { } private void prepareEncryption() throws IOException, PGPException { - if (encryptionKeys.isEmpty()) { + if (encryptionKeys.isEmpty() && encryptionPassphrases.isEmpty()) { return; } @@ -143,6 +148,10 @@ public final class EncryptionStream extends OutputStream { encryptedDataGenerator.addMethod(new BcPublicKeyKeyEncryptionMethodGenerator(key)); } + for (Passphrase passphrase : encryptionPassphrases) { + encryptedDataGenerator.addMethod(new BcPBEKeyEncryptionMethodGenerator(passphrase.getChars())); + } + publicKeyEncryptedStream = encryptedDataGenerator.open(outermostStream, new byte[BUFFER_SIZE]); outermostStream = publicKeyEncryptedStream; } diff --git a/pgpainless-core/src/test/java/org/pgpainless/symmetric_encryption/LegacySymmetricEncryptionTest.java b/pgpainless-core/src/test/java/org/pgpainless/symmetric_encryption/LegacySymmetricEncryptionTest.java new file mode 100644 index 00000000..c8b44c7b --- /dev/null +++ b/pgpainless-core/src/test/java/org/pgpainless/symmetric_encryption/LegacySymmetricEncryptionTest.java @@ -0,0 +1,67 @@ +/* + * Copyright 2018 Paul Schaub. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.pgpainless.symmetric_encryption; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.util.logging.Level; +import java.util.logging.Logger; + +import org.bouncycastle.bcpg.ArmoredOutputStream; +import org.bouncycastle.openpgp.PGPException; +import org.junit.jupiter.api.Test; +import org.pgpainless.PGPainless; +import org.pgpainless.algorithm.SymmetricKeyAlgorithm; +import org.pgpainless.util.Passphrase; + +public class LegacySymmetricEncryptionTest { + + private static final Logger LOGGER = Logger.getLogger(LegacySymmetricEncryptionTest.class.getName()); + + private static final String message = + "I grew up with the understanding that the world " + + "I lived in was one where people enjoyed a sort of freedom " + + "to communicate with each other in privacy, without it " + + "being monitored, without it being measured or analyzed " + + "or sort of judged by these shadowy figures or systems, " + + "any time they mention anything that travels across " + + "public lines.\n" + + "\n" + + "- Edward Snowden -"; + + @SuppressWarnings("deprecation") + @Test + public void testSymmetricEncryptionDecryption() throws IOException, PGPException { + byte[] plain = message.getBytes(); + String password = "choose_a_better_password_please"; + Passphrase passphrase = new Passphrase(password.toCharArray()); + byte[] enc = PGPainless.encryptWithPassword(plain, passphrase, SymmetricKeyAlgorithm.AES_128); + ByteArrayOutputStream out = new ByteArrayOutputStream(); + ArmoredOutputStream armor = new ArmoredOutputStream(out); + armor.write(enc); + armor.flush(); + armor.close(); + + // Print cipher text for validation with GnuPG. + LOGGER.log(Level.INFO, String.format("Use ciphertext below for manual validation with GnuPG " + + "(passphrase = '%s').\n\n%s", password, new String(out.toByteArray()))); + + byte[] plain2 = PGPainless.decryptWithPassword(enc, passphrase); + assertArrayEquals(plain, plain2); + } +} diff --git a/pgpainless-core/src/test/java/org/pgpainless/symmetric_encryption/SymmetricEncryptionTest.java b/pgpainless-core/src/test/java/org/pgpainless/symmetric_encryption/SymmetricEncryptionTest.java index 7df26196..ea4c35bd 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/symmetric_encryption/SymmetricEncryptionTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/symmetric_encryption/SymmetricEncryptionTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 Paul Schaub. + * Copyright 2020 Paul Schaub. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,50 +17,82 @@ package org.pgpainless.symmetric_encryption; import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; -import java.util.logging.Level; -import java.util.logging.Logger; +import java.nio.charset.StandardCharsets; -import org.bouncycastle.bcpg.ArmoredOutputStream; import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.PGPPublicKeyRing; +import org.bouncycastle.openpgp.PGPSecretKeyRingCollection; +import org.bouncycastle.util.io.Streams; import org.junit.jupiter.api.Test; import org.pgpainless.PGPainless; -import org.pgpainless.algorithm.SymmetricKeyAlgorithm; +import org.pgpainless.decryption_verification.DecryptionStream; +import org.pgpainless.encryption_signing.EncryptionStream; +import org.pgpainless.key.TestKeys; +import org.pgpainless.key.protection.KeyRingProtectionSettings; +import org.pgpainless.key.protection.PasswordBasedSecretKeyRingProtector; +import org.pgpainless.key.protection.SecretKeyRingProtector; +import org.pgpainless.key.protection.passphrase_provider.SolitaryPassphraseProvider; import org.pgpainless.util.Passphrase; +/** + * Test parallel symmetric and public key encryption/decryption. + */ public class SymmetricEncryptionTest { - private static final Logger LOGGER = Logger.getLogger(SymmetricEncryptionTest.class.getName()); - - private static final String message = - "I grew up with the understanding that the world " + - "I lived in was one where people enjoyed a sort of freedom " + - "to communicate with each other in privacy, without it " + - "being monitored, without it being measured or analyzed " + - "or sort of judged by these shadowy figures or systems, " + - "any time they mention anything that travels across " + - "public lines.\n" + - "\n" + - "- Edward Snowden -"; - @Test - public void testSymmetricEncryptionDecryption() throws IOException, PGPException { - byte[] plain = message.getBytes(); - String password = "choose_a_better_password_please"; - Passphrase passphrase = new Passphrase(password.toCharArray()); - byte[] enc = PGPainless.encryptWithPassword(plain, passphrase, SymmetricKeyAlgorithm.AES_128); - ByteArrayOutputStream out = new ByteArrayOutputStream(); - ArmoredOutputStream armor = new ArmoredOutputStream(out); - armor.write(enc); - armor.flush(); - armor.close(); + public void test() throws IOException, PGPException { + byte[] plaintext = "This is a secret message".getBytes(StandardCharsets.UTF_8); + ByteArrayInputStream plaintextIn = new ByteArrayInputStream(plaintext); + PGPPublicKeyRing encryptionKey = TestKeys.getCryptiePublicKeyRing(); + Passphrase encryptionPassphrase = Passphrase.fromPassword("greenBeans"); - // Print cipher text for validation with GnuPG. - LOGGER.log(Level.INFO, String.format("Use ciphertext below for manual validation with GnuPG " + - "(passphrase = '%s').\n\n%s", password, new String(out.toByteArray()))); + ByteArrayOutputStream ciphertextOut = new ByteArrayOutputStream(); + EncryptionStream encryptor = PGPainless.encryptAndOrSign().onOutputStream(ciphertextOut) + .forPassphrases(encryptionPassphrase) + .and() + .toRecipients(encryptionKey) + .usingSecureAlgorithms() + .doNotSign() + .noArmor(); - byte[] plain2 = PGPainless.decryptWithPassword(enc, passphrase); - assertArrayEquals(plain, plain2); + Streams.pipeAll(plaintextIn, encryptor); + encryptor.close(); + + byte[] ciphertext = ciphertextOut.toByteArray(); + + // Test symmetric decryption + DecryptionStream decryptor = PGPainless.decryptAndOrVerify() + .onInputStream(new ByteArrayInputStream(ciphertext)) + .decryptWith(encryptionPassphrase) + .doNotVerify() + .build(); + + ByteArrayOutputStream decrypted = new ByteArrayOutputStream(); + + Streams.pipeAll(decryptor, decrypted); + decryptor.close(); + + assertArrayEquals(plaintext, decrypted.toByteArray()); + + // Test public key decryption + PGPSecretKeyRingCollection decryptionKeys = TestKeys.getCryptieSecretKeyRingCollection(); + SecretKeyRingProtector protector = new PasswordBasedSecretKeyRingProtector( + KeyRingProtectionSettings.secureDefaultSettings(), + new SolitaryPassphraseProvider(Passphrase.fromPassword(TestKeys.CRYPTIE_PASSWORD))); + decryptor = PGPainless.decryptAndOrVerify() + .onInputStream(new ByteArrayInputStream(ciphertext)) + .decryptWith(protector, decryptionKeys) + .doNotVerify() + .build(); + + decrypted = new ByteArrayOutputStream(); + + Streams.pipeAll(decryptor, decrypted); + decryptor.close(); + + assertArrayEquals(plaintext, decrypted.toByteArray()); } } diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/commands/Encrypt.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/commands/Encrypt.java index 877bcac5..3f4fb812 100644 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/commands/Encrypt.java +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/commands/Encrypt.java @@ -15,6 +15,17 @@ */ package org.pgpainless.sop.commands; +import static org.pgpainless.sop.Print.err_ln; +import static org.pgpainless.sop.Print.print_ln; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.HashMap; +import java.util.Map; +import java.util.Scanner; + import org.bouncycastle.openpgp.PGPException; import org.bouncycastle.openpgp.PGPPublicKeyRing; import org.bouncycastle.openpgp.PGPSecretKey; @@ -32,17 +43,6 @@ import org.pgpainless.key.protection.PassphraseMapKeyRingProtector; import org.pgpainless.util.Passphrase; import picocli.CommandLine; -import java.io.File; -import java.io.FileInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.util.HashMap; -import java.util.Map; -import java.util.Scanner; - -import static org.pgpainless.sop.Print.err_ln; -import static org.pgpainless.sop.Print.print_ln; - @CommandLine.Command(name = "encrypt", description = "Encrypt a message from standard input") public class Encrypt implements Runnable { @@ -107,6 +107,11 @@ public class Encrypt implements Runnable { System.exit(1); } } + Passphrase[] passphraseArray = new Passphrase[withPassword.length]; + for (int i = 0; i < withPassword.length; i++) { + String password = withPassword[i]; + passphraseArray[i] = Passphrase.fromPassword(password); + } Map passphraseMap = new HashMap<>(); Scanner scanner = null; @@ -137,6 +142,8 @@ public class Encrypt implements Runnable { EncryptionBuilderInterface.DetachedSign builder = PGPainless.encryptAndOrSign() .onOutputStream(System.out) .toRecipients(publicKeys) + .and() + .forPassphrases(passphraseArray) .usingSecureAlgorithms(); EncryptionBuilderInterface.Armor builder_armor; if (signWith.length != 0) { From c7ede0fc8ac8ed7277774d352650401589632969 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Sat, 26 Dec 2020 23:36:33 +0100 Subject: [PATCH 22/25] Add junit test for symmetric decryption --- .../DecryptionStreamFactory.java | 9 ++- ...ultiPassphraseSymmetricEncryptionTest.java | 65 +++++++++++++++++++ 2 files changed, 72 insertions(+), 2 deletions(-) create mode 100644 pgpainless-core/src/test/java/org/pgpainless/symmetric_encryption/MultiPassphraseSymmetricEncryptionTest.java diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionStreamFactory.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionStreamFactory.java index 130eb537..b991d316 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionStreamFactory.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionStreamFactory.java @@ -201,7 +201,12 @@ public final class DecryptionStreamFactory { pbeEncryptedData.getSymmetricAlgorithm(passphraseDecryptor)); resultBuilder.setSymmetricKeyAlgorithm(symmetricKeyAlgorithm); resultBuilder.setIntegrityProtected(pbeEncryptedData.isIntegrityProtected()); - return pbeEncryptedData.getDataStream(passphraseDecryptor); + + try { + return pbeEncryptedData.getDataStream(passphraseDecryptor); + } catch (PGPException e) { + LOGGER.log(LEVEL, "Probable passphrase mismatch, skip PBE encrypted data block", e); + } } } else if (encryptedData instanceof PGPPublicKeyEncryptedData) { @@ -225,7 +230,7 @@ public final class DecryptionStreamFactory { } if (decryptionKey == null) { - throw new PGPException("Decryption failed - No suitable decryption key found"); + throw new PGPException("Decryption failed - No suitable decryption key or passphrase found"); } PublicKeyDataDecryptorFactory keyDecryptor = new BcPublicKeyDataDecryptorFactory(decryptionKey); diff --git a/pgpainless-core/src/test/java/org/pgpainless/symmetric_encryption/MultiPassphraseSymmetricEncryptionTest.java b/pgpainless-core/src/test/java/org/pgpainless/symmetric_encryption/MultiPassphraseSymmetricEncryptionTest.java new file mode 100644 index 00000000..aee992f2 --- /dev/null +++ b/pgpainless-core/src/test/java/org/pgpainless/symmetric_encryption/MultiPassphraseSymmetricEncryptionTest.java @@ -0,0 +1,65 @@ +/* + * Copyright 2020 Paul Schaub. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.pgpainless.symmetric_encryption; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; + +import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.util.io.Streams; +import org.junit.jupiter.api.Test; +import org.pgpainless.PGPainless; +import org.pgpainless.decryption_verification.DecryptionStream; +import org.pgpainless.encryption_signing.EncryptionStream; +import org.pgpainless.util.Passphrase; + +public class MultiPassphraseSymmetricEncryptionTest { + + @Test + public void test() throws IOException, PGPException { + String message = "Here we test if during decryption of a message that was encrypted with two passphrases, " + + "the decryptor finds the session key encrypted for the right passphrase."; + ByteArrayInputStream plaintextIn = new ByteArrayInputStream(message.getBytes(StandardCharsets.UTF_8)); + ByteArrayOutputStream ciphertextOut = new ByteArrayOutputStream(); + EncryptionStream encryptor = PGPainless.encryptAndOrSign() + .onOutputStream(ciphertextOut) + .forPassphrases(Passphrase.fromPassword("p1"), Passphrase.fromPassword("p2")) + .usingSecureAlgorithms() + .doNotSign() + .noArmor(); + + Streams.pipeAll(plaintextIn, encryptor); + encryptor.close(); + + byte[] ciphertext = ciphertextOut.toByteArray(); + + // decrypting the p1 package with p2 first will not work. Test if it is handled correctly. + for (Passphrase passphrase : new Passphrase[] {Passphrase.fromPassword("p2"), Passphrase.fromPassword("p1")}) { + DecryptionStream decryptor = PGPainless.decryptAndOrVerify().onInputStream(new ByteArrayInputStream(ciphertext)) + .decryptWith(passphrase) + .doNotVerify() + .build(); + + ByteArrayOutputStream plaintextOut = new ByteArrayOutputStream(); + + Streams.pipeAll(decryptor, plaintextOut); + + decryptor.close(); + } + } +} From 1c1f9d49ab6a85a045913314e637dba746c1d485 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Sun, 27 Dec 2020 01:56:18 +0100 Subject: [PATCH 23/25] Introduce ImplementationFactory --- .../DecryptionBuilder.java | 5 +- .../DecryptionStreamFactory.java | 30 ++-- .../encryption_signing/EncryptionStream.java | 35 ++-- .../BcCryptoEngineImplementation.java | 145 ++++++++++++++++ .../CryptoEngineImplementation.java | 91 ++++++++++ .../implementation/ImplementationFactory.java | 15 ++ .../JceCryptoEngineImplementation.java | 162 ++++++++++++++++++ .../key/generation/KeyRingBuilder.java | 30 ++-- .../secretkeyring/SecretKeyRingEditor.java | 26 +-- .../pgpainless/key/parsing/KeyRingReader.java | 10 +- .../CallbackBasedKeyringProtector.java | 52 ++++++ .../PassphraseMapKeyRingProtector.java | 8 +- .../PasswordBasedSecretKeyRingProtector.java | 22 +-- .../protection/SecretKeyRingProtector.java | 5 +- .../protection/SecretKeyRingProtector2.java | 13 ++ .../SecretKeyRingProtectorAdapter.java | 19 ++ .../MapBasedPassphraseProvider.java | 1 + .../SecretKeyPassphraseProvider.java | 6 +- .../main/java/org/pgpainless/util/BCUtil.java | 10 +- .../PassphraseProtectedKeyTest.java | 3 +- .../java/org/pgpainless/sop/SopKeyUtil.java | 29 ++++ .../org/pgpainless/sop/commands/Decrypt.java | 29 ++++ 22 files changed, 655 insertions(+), 91 deletions(-) create mode 100644 pgpainless-core/src/main/java/org/pgpainless/implementation/BcCryptoEngineImplementation.java create mode 100644 pgpainless-core/src/main/java/org/pgpainless/implementation/CryptoEngineImplementation.java create mode 100644 pgpainless-core/src/main/java/org/pgpainless/implementation/ImplementationFactory.java create mode 100644 pgpainless-core/src/main/java/org/pgpainless/implementation/JceCryptoEngineImplementation.java create mode 100644 pgpainless-core/src/main/java/org/pgpainless/key/protection/CallbackBasedKeyringProtector.java create mode 100644 pgpainless-core/src/main/java/org/pgpainless/key/protection/SecretKeyRingProtector2.java create mode 100644 pgpainless-core/src/main/java/org/pgpainless/key/protection/SecretKeyRingProtectorAdapter.java create mode 100644 pgpainless-sop/src/main/java/org/pgpainless/sop/SopKeyUtil.java diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionBuilder.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionBuilder.java index f78e03c4..cd739017 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionBuilder.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionBuilder.java @@ -34,7 +34,7 @@ import org.bouncycastle.openpgp.PGPSignature; import org.bouncycastle.openpgp.PGPSignatureList; import org.bouncycastle.openpgp.PGPUtil; import org.bouncycastle.openpgp.operator.KeyFingerPrintCalculator; -import org.bouncycastle.openpgp.operator.bc.BcKeyFingerprintCalculator; +import org.pgpainless.implementation.ImplementationFactory; import org.pgpainless.key.OpenPgpV4Fingerprint; import org.pgpainless.key.protection.SecretKeyRingProtector; import org.pgpainless.util.Passphrase; @@ -49,7 +49,8 @@ public class DecryptionBuilder implements DecryptionBuilderInterface { private Set verificationKeys = new HashSet<>(); private MissingPublicKeyCallback missingPublicKeyCallback = null; - private final KeyFingerPrintCalculator keyFingerPrintCalculator = new BcKeyFingerprintCalculator(); + private final KeyFingerPrintCalculator keyFingerPrintCalculator = + ImplementationFactory.getInstance().getKeyFingerprintCalculator(); @Override public DecryptWith onInputStream(@Nonnull InputStream inputStream) { diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionStreamFactory.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionStreamFactory.java index b991d316..92b0527c 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionStreamFactory.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionStreamFactory.java @@ -15,8 +15,6 @@ */ package org.pgpainless.decryption_verification; -import javax.annotation.Nonnull; -import javax.annotation.Nullable; import java.io.IOException; import java.io.InputStream; import java.util.Collections; @@ -28,6 +26,8 @@ import java.util.Map; import java.util.Set; import java.util.logging.Level; import java.util.logging.Logger; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; import org.bouncycastle.openpgp.PGPCompressedData; import org.bouncycastle.openpgp.PGPEncryptedData; @@ -50,13 +50,9 @@ import org.bouncycastle.openpgp.operator.KeyFingerPrintCalculator; import org.bouncycastle.openpgp.operator.PBEDataDecryptorFactory; import org.bouncycastle.openpgp.operator.PGPContentVerifierBuilderProvider; import org.bouncycastle.openpgp.operator.PublicKeyDataDecryptorFactory; -import org.bouncycastle.openpgp.operator.bc.BcKeyFingerprintCalculator; -import org.bouncycastle.openpgp.operator.bc.BcPBEDataDecryptorFactory; -import org.bouncycastle.openpgp.operator.bc.BcPGPContentVerifierBuilderProvider; -import org.bouncycastle.openpgp.operator.bc.BcPGPDigestCalculatorProvider; -import org.bouncycastle.openpgp.operator.bc.BcPublicKeyDataDecryptorFactory; import org.pgpainless.algorithm.CompressionAlgorithm; import org.pgpainless.algorithm.SymmetricKeyAlgorithm; +import org.pgpainless.implementation.ImplementationFactory; import org.pgpainless.key.OpenPgpV4Fingerprint; import org.pgpainless.key.protection.SecretKeyRingProtector; import org.pgpainless.util.Passphrase; @@ -73,8 +69,8 @@ public final class DecryptionStreamFactory { private final MissingPublicKeyCallback missingPublicKeyCallback; private final OpenPgpMetadata.Builder resultBuilder = OpenPgpMetadata.getBuilder(); - private final PGPContentVerifierBuilderProvider verifierBuilderProvider = new BcPGPContentVerifierBuilderProvider(); - private final KeyFingerPrintCalculator keyFingerprintCalculator = new BcKeyFingerprintCalculator(); + private static final PGPContentVerifierBuilderProvider verifierBuilderProvider = ImplementationFactory.getInstance().getPGPContentVerifierBuilderProvider(); + private static final KeyFingerPrintCalculator keyFingerprintCalculator = ImplementationFactory.getInstance().getKeyFingerprintCalculator(); private final Map verifiableOnePassSignatures = new HashMap<>(); private DecryptionStreamFactory(@Nullable PGPSecretKeyRingCollection decryptionKeys, @@ -108,13 +104,13 @@ public final class DecryptionStreamFactory { if (signingKey == null) { continue; } - signature.init(new BcPGPContentVerifierBuilderProvider(), signingKey); + signature.init(ImplementationFactory.getInstance().getPGPContentVerifierBuilderProvider(), signingKey); factory.resultBuilder.addDetachedSignature( new DetachedSignature(signature, new OpenPgpV4Fingerprint(signingKey))); } } else { PGPObjectFactory objectFactory = new PGPObjectFactory( - PGPUtil.getDecoderStream(inputStream), new BcKeyFingerprintCalculator()); + PGPUtil.getDecoderStream(inputStream), keyFingerprintCalculator); pgpInputStream = factory.processPGPPackets(objectFactory); } return new DecryptionStream(pgpInputStream, factory.resultBuilder); @@ -195,8 +191,8 @@ public final class DecryptionStreamFactory { PGPPBEEncryptedData pbeEncryptedData = (PGPPBEEncryptedData) encryptedData; if (decryptionPassphrase != null) { - PBEDataDecryptorFactory passphraseDecryptor = new BcPBEDataDecryptorFactory( - decryptionPassphrase.getChars(), new BcPGPDigestCalculatorProvider()); + PBEDataDecryptorFactory passphraseDecryptor = ImplementationFactory.getInstance() + .getPBEDataDecryptorFactory(decryptionPassphrase); SymmetricKeyAlgorithm symmetricKeyAlgorithm = SymmetricKeyAlgorithm.fromId( pbeEncryptedData.getSymmetricAlgorithm(passphraseDecryptor)); resultBuilder.setSymmetricKeyAlgorithm(symmetricKeyAlgorithm); @@ -233,7 +229,8 @@ public final class DecryptionStreamFactory { throw new PGPException("Decryption failed - No suitable decryption key or passphrase found"); } - PublicKeyDataDecryptorFactory keyDecryptor = new BcPublicKeyDataDecryptorFactory(decryptionKey); + PublicKeyDataDecryptorFactory keyDecryptor = ImplementationFactory.getInstance() + .getPublicKeyDataDecryptorFactory(decryptionKey); SymmetricKeyAlgorithm symmetricKeyAlgorithm = SymmetricKeyAlgorithm .fromId(encryptedSessionKey.getSymmetricAlgorithm(keyDecryptor)); @@ -275,9 +272,10 @@ public final class DecryptionStreamFactory { } signature.init(verifierBuilderProvider, verificationKey); - OnePassSignature onePassSignature = new OnePassSignature(signature, new OpenPgpV4Fingerprint(verificationKey)); + OpenPgpV4Fingerprint fingerprint = new OpenPgpV4Fingerprint(verificationKey); + OnePassSignature onePassSignature = new OnePassSignature(signature, fingerprint); resultBuilder.addOnePassSignature(onePassSignature); - verifiableOnePassSignatures.put(new OpenPgpV4Fingerprint(verificationKey), onePassSignature); + verifiableOnePassSignatures.put(fingerprint, onePassSignature); } private PGPPublicKey findSignatureVerificationKey(long keyId) { diff --git a/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/EncryptionStream.java b/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/EncryptionStream.java index 39a10e0f..e3c90040 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/EncryptionStream.java +++ b/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/EncryptionStream.java @@ -37,16 +37,19 @@ import org.bouncycastle.openpgp.PGPPrivateKey; import org.bouncycastle.openpgp.PGPPublicKey; import org.bouncycastle.openpgp.PGPSignature; import org.bouncycastle.openpgp.PGPSignatureGenerator; -import org.bouncycastle.openpgp.operator.bc.BcPBEKeyEncryptionMethodGenerator; -import org.bouncycastle.openpgp.operator.bc.BcPGPContentSignerBuilder; +import org.bouncycastle.openpgp.operator.PBEKeyEncryptionMethodGenerator; +import org.bouncycastle.openpgp.operator.PGPContentSignerBuilder; +import org.bouncycastle.openpgp.operator.PGPDataEncryptorBuilder; +import org.bouncycastle.openpgp.operator.PublicKeyKeyEncryptionMethodGenerator; import org.bouncycastle.openpgp.operator.bc.BcPGPDataEncryptorBuilder; -import org.bouncycastle.openpgp.operator.bc.BcPublicKeyKeyEncryptionMethodGenerator; +import org.bouncycastle.openpgp.operator.jcajce.JcePGPDataEncryptorBuilder; import org.pgpainless.algorithm.CompressionAlgorithm; import org.pgpainless.algorithm.HashAlgorithm; import org.pgpainless.algorithm.SignatureType; import org.pgpainless.algorithm.SymmetricKeyAlgorithm; import org.pgpainless.decryption_verification.DetachedSignature; import org.pgpainless.decryption_verification.OpenPgpMetadata; +import org.pgpainless.implementation.ImplementationFactory; import org.pgpainless.key.OpenPgpV4Fingerprint; import org.pgpainless.util.Passphrase; @@ -136,20 +139,30 @@ public final class EncryptionStream extends OutputStream { } LOGGER.log(LEVEL, "At least one encryption key is available -> encrypt using " + symmetricKeyAlgorithm); - BcPGPDataEncryptorBuilder dataEncryptorBuilder = - new BcPGPDataEncryptorBuilder(symmetricKeyAlgorithm.getAlgorithmId()); + PGPDataEncryptorBuilder dataEncryptorBuilder = + ImplementationFactory.getInstance().getPGPDataEncryptorBuilder(symmetricKeyAlgorithm); + + // Simplify once https://github.com/bcgit/bc-java/pull/859 is merged + if (dataEncryptorBuilder instanceof BcPGPDataEncryptorBuilder) { + ((BcPGPDataEncryptorBuilder) dataEncryptorBuilder).setWithIntegrityPacket(true); + } else if (dataEncryptorBuilder instanceof JcePGPDataEncryptorBuilder) { + ((JcePGPDataEncryptorBuilder) dataEncryptorBuilder).setWithIntegrityPacket(true); + } - dataEncryptorBuilder.setWithIntegrityPacket(true); PGPEncryptedDataGenerator encryptedDataGenerator = new PGPEncryptedDataGenerator(dataEncryptorBuilder); for (PGPPublicKey key : encryptionKeys) { LOGGER.log(LEVEL, "Encrypt for key " + Long.toHexString(key.getKeyID())); - encryptedDataGenerator.addMethod(new BcPublicKeyKeyEncryptionMethodGenerator(key)); + PublicKeyKeyEncryptionMethodGenerator keyEncryption = + ImplementationFactory.getInstance().getPublicKeyKeyEncryptionMethodGenerator(key); + encryptedDataGenerator.addMethod(keyEncryption); } for (Passphrase passphrase : encryptionPassphrases) { - encryptedDataGenerator.addMethod(new BcPBEKeyEncryptionMethodGenerator(passphrase.getChars())); + PBEKeyEncryptionMethodGenerator passphraseEncryption = + ImplementationFactory.getInstance().getPBEKeyEncryptionMethodGenerator(passphrase); + encryptedDataGenerator.addMethod(passphraseEncryption); } publicKeyEncryptedStream = encryptedDataGenerator.open(outermostStream, new byte[BUFFER_SIZE]); @@ -165,8 +178,10 @@ public final class EncryptionStream extends OutputStream { for (OpenPgpV4Fingerprint fingerprint : signingKeys.keySet()) { PGPPrivateKey privateKey = signingKeys.get(fingerprint); LOGGER.log(LEVEL, "Sign using key " + fingerprint); - BcPGPContentSignerBuilder contentSignerBuilder = new BcPGPContentSignerBuilder( - privateKey.getPublicKeyPacket().getAlgorithm(), hashAlgorithm.getAlgorithmId()); + PGPContentSignerBuilder contentSignerBuilder = ImplementationFactory.getInstance() + .getPGPContentSignerBuilder( + privateKey.getPublicKeyPacket().getAlgorithm(), + hashAlgorithm.getAlgorithmId()); PGPSignatureGenerator signatureGenerator = new PGPSignatureGenerator(contentSignerBuilder); signatureGenerator.init(signatureType.getCode(), privateKey); diff --git a/pgpainless-core/src/main/java/org/pgpainless/implementation/BcCryptoEngineImplementation.java b/pgpainless-core/src/main/java/org/pgpainless/implementation/BcCryptoEngineImplementation.java new file mode 100644 index 00000000..66829d07 --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/implementation/BcCryptoEngineImplementation.java @@ -0,0 +1,145 @@ +package org.pgpainless.implementation; + +import java.security.KeyPair; +import java.util.Date; + +import org.bouncycastle.crypto.AsymmetricCipherKeyPair; +import org.bouncycastle.crypto.params.AsymmetricKeyParameter; +import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.PGPKeyPair; +import org.bouncycastle.openpgp.PGPPrivateKey; +import org.bouncycastle.openpgp.PGPPublicKey; +import org.bouncycastle.openpgp.PGPSecretKey; +import org.bouncycastle.openpgp.operator.KeyFingerPrintCalculator; +import org.bouncycastle.openpgp.operator.PBEDataDecryptorFactory; +import org.bouncycastle.openpgp.operator.PBEKeyEncryptionMethodGenerator; +import org.bouncycastle.openpgp.operator.PBESecretKeyDecryptor; +import org.bouncycastle.openpgp.operator.PBESecretKeyEncryptor; +import org.bouncycastle.openpgp.operator.PGPContentSignerBuilder; +import org.bouncycastle.openpgp.operator.PGPContentVerifierBuilderProvider; +import org.bouncycastle.openpgp.operator.PGPDataEncryptorBuilder; +import org.bouncycastle.openpgp.operator.PGPDigestCalculator; +import org.bouncycastle.openpgp.operator.PublicKeyDataDecryptorFactory; +import org.bouncycastle.openpgp.operator.PublicKeyKeyEncryptionMethodGenerator; +import org.bouncycastle.openpgp.operator.bc.BcKeyFingerprintCalculator; +import org.bouncycastle.openpgp.operator.bc.BcPBEDataDecryptorFactory; +import org.bouncycastle.openpgp.operator.bc.BcPBEKeyEncryptionMethodGenerator; +import org.bouncycastle.openpgp.operator.bc.BcPBESecretKeyDecryptorBuilder; +import org.bouncycastle.openpgp.operator.bc.BcPBESecretKeyEncryptorBuilder; +import org.bouncycastle.openpgp.operator.bc.BcPGPContentSignerBuilder; +import org.bouncycastle.openpgp.operator.bc.BcPGPContentVerifierBuilderProvider; +import org.bouncycastle.openpgp.operator.bc.BcPGPDataEncryptorBuilder; +import org.bouncycastle.openpgp.operator.bc.BcPGPDigestCalculatorProvider; +import org.bouncycastle.openpgp.operator.bc.BcPGPKeyConverter; +import org.bouncycastle.openpgp.operator.bc.BcPGPKeyPair; +import org.bouncycastle.openpgp.operator.bc.BcPublicKeyDataDecryptorFactory; +import org.bouncycastle.openpgp.operator.bc.BcPublicKeyKeyEncryptionMethodGenerator; +import org.bouncycastle.openpgp.operator.jcajce.JcaPGPKeyPair; +import org.pgpainless.algorithm.HashAlgorithm; +import org.pgpainless.algorithm.PublicKeyAlgorithm; +import org.pgpainless.algorithm.SymmetricKeyAlgorithm; +import org.pgpainless.util.Passphrase; + +public class BcCryptoEngineImplementation implements CryptoEngineImplementation { + + @Override + public PBESecretKeyEncryptor getPBESecretKeyEncryptor(PGPSecretKey secretKey, Passphrase passphrase) + throws PGPException { + return new BcPBESecretKeyEncryptorBuilder(secretKey.getKeyEncryptionAlgorithm(), + getPGPDigestCalculator(secretKey.getS2K().getHashAlgorithm()), + (int) secretKey.getS2K().getIterationCount()) + .build(passphrase.getChars()); + } + + @Override + public PBESecretKeyEncryptor getPBESecretKeyEncryptor(SymmetricKeyAlgorithm symmetricKeyAlgorithm, + PGPDigestCalculator digestCalculator, + Passphrase passphrase) { + return new BcPBESecretKeyEncryptorBuilder(symmetricKeyAlgorithm.getAlgorithmId(), digestCalculator) + .build(passphrase.getChars()); + } + + @Override + public PBESecretKeyDecryptor getPBESecretKeyDecryptor(Passphrase passphrase) { + return new BcPBESecretKeyDecryptorBuilder(getPGPDigestCalculatorProvider()) + .build(passphrase.getChars()); + } + + @Override + public BcPGPDigestCalculatorProvider getPGPDigestCalculatorProvider() { + return new BcPGPDigestCalculatorProvider(); + } + + @Override + public PGPContentVerifierBuilderProvider getPGPContentVerifierBuilderProvider() { + return new BcPGPContentVerifierBuilderProvider(); + } + + @Override + public PGPContentSignerBuilder getPGPContentSignerBuilder(int keyAlgorithm, int hashAlgorithm) { + return new BcPGPContentSignerBuilder(keyAlgorithm, hashAlgorithm); + } + + @Override + public KeyFingerPrintCalculator getKeyFingerprintCalculator() { + return new BcKeyFingerprintCalculator(); + } + + @Override + public PBEDataDecryptorFactory getPBEDataDecryptorFactory(Passphrase passphrase) { + return new BcPBEDataDecryptorFactory(passphrase.getChars(), getPGPDigestCalculatorProvider()); + } + + @Override + public PublicKeyDataDecryptorFactory getPublicKeyDataDecryptorFactory(PGPPrivateKey privateKey) { + return new BcPublicKeyDataDecryptorFactory(privateKey); + } + + @Override + public PublicKeyKeyEncryptionMethodGenerator getPublicKeyKeyEncryptionMethodGenerator(PGPPublicKey key) { + return new BcPublicKeyKeyEncryptionMethodGenerator(key); + } + + @Override + public PBEKeyEncryptionMethodGenerator getPBEKeyEncryptionMethodGenerator(Passphrase passphrase) { + return new BcPBEKeyEncryptionMethodGenerator(passphrase.getChars()); + } + + @Override + public PGPDataEncryptorBuilder getPGPDataEncryptorBuilder(int symmetricKeyAlgorithm) { + return new BcPGPDataEncryptorBuilder(symmetricKeyAlgorithm); + } + + @Override + public PGPKeyPair getPGPKeyPair(PublicKeyAlgorithm algorithm, KeyPair keyPair, Date creationDate) + throws PGPException { + return new BcPGPKeyPair(algorithm.getAlgorithmId(), jceToBcKeyPair(algorithm, keyPair, creationDate), creationDate); + } + + @Override + public PGPKeyPair getPGPKeyPair(PublicKeyAlgorithm algorithm, AsymmetricCipherKeyPair keyPair, Date creationDate) throws PGPException { + return new BcPGPKeyPair(algorithm.getAlgorithmId(), keyPair, creationDate); + } + + @Override + public PBESecretKeyEncryptor getPBESecretKeyEncryptor(SymmetricKeyAlgorithm encryptionAlgorithm, HashAlgorithm hashAlgorithm, int s2kCount, Passphrase passphrase) throws PGPException { + return new BcPBESecretKeyEncryptorBuilder( + encryptionAlgorithm.getAlgorithmId(), + getPGPDigestCalculator(hashAlgorithm), + s2kCount) + .build(passphrase.getChars()); + } + + // TODO: Find a better conversion method that does not depend on JcaPGPKeyPair. + private AsymmetricCipherKeyPair jceToBcKeyPair(PublicKeyAlgorithm algorithm, + KeyPair keyPair, + Date creationDate) throws PGPException { + BcPGPKeyConverter converter = new BcPGPKeyConverter(); + + PGPKeyPair pair = new JcaPGPKeyPair(algorithm.getAlgorithmId(), keyPair, creationDate); + AsymmetricKeyParameter publicKey = converter.getPublicKey(pair.getPublicKey()); + AsymmetricKeyParameter privateKey = converter.getPrivateKey(pair.getPrivateKey()); + + return new AsymmetricCipherKeyPair(publicKey, privateKey); + } +} diff --git a/pgpainless-core/src/main/java/org/pgpainless/implementation/CryptoEngineImplementation.java b/pgpainless-core/src/main/java/org/pgpainless/implementation/CryptoEngineImplementation.java new file mode 100644 index 00000000..154abb65 --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/implementation/CryptoEngineImplementation.java @@ -0,0 +1,91 @@ +package org.pgpainless.implementation; + +import java.io.IOException; +import java.security.KeyPair; +import java.security.NoSuchAlgorithmException; +import java.security.spec.InvalidKeySpecException; +import java.util.Date; + +import org.bouncycastle.crypto.AsymmetricCipherKeyPair; +import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.PGPKeyPair; +import org.bouncycastle.openpgp.PGPPrivateKey; +import org.bouncycastle.openpgp.PGPPublicKey; +import org.bouncycastle.openpgp.PGPSecretKey; +import org.bouncycastle.openpgp.operator.KeyFingerPrintCalculator; +import org.bouncycastle.openpgp.operator.PBEDataDecryptorFactory; +import org.bouncycastle.openpgp.operator.PBEKeyEncryptionMethodGenerator; +import org.bouncycastle.openpgp.operator.PBESecretKeyDecryptor; +import org.bouncycastle.openpgp.operator.PBESecretKeyEncryptor; +import org.bouncycastle.openpgp.operator.PGPContentSignerBuilder; +import org.bouncycastle.openpgp.operator.PGPContentVerifierBuilderProvider; +import org.bouncycastle.openpgp.operator.PGPDataEncryptorBuilder; +import org.bouncycastle.openpgp.operator.PGPDigestCalculator; +import org.bouncycastle.openpgp.operator.PGPDigestCalculatorProvider; +import org.bouncycastle.openpgp.operator.PublicKeyDataDecryptorFactory; +import org.bouncycastle.openpgp.operator.PublicKeyKeyEncryptionMethodGenerator; +import org.pgpainless.algorithm.HashAlgorithm; +import org.pgpainless.algorithm.PublicKeyAlgorithm; +import org.pgpainless.algorithm.SymmetricKeyAlgorithm; +import org.pgpainless.util.Passphrase; + +public interface CryptoEngineImplementation { + + PBESecretKeyEncryptor getPBESecretKeyEncryptor(PGPSecretKey secretKey, Passphrase passphrase) throws PGPException; + + default PBESecretKeyEncryptor getPBESecretKeyEncryptor(SymmetricKeyAlgorithm symmetricKeyAlgorithm, + Passphrase passphrase) + throws PGPException { + return getPBESecretKeyEncryptor(symmetricKeyAlgorithm, + getPGPDigestCalculator(HashAlgorithm.SHA1), passphrase); + } + + PBESecretKeyEncryptor getPBESecretKeyEncryptor(SymmetricKeyAlgorithm symmetricKeyAlgorithm, + PGPDigestCalculator digestCalculator, + Passphrase passphrase); + + PBESecretKeyDecryptor getPBESecretKeyDecryptor(Passphrase passphrase) throws PGPException; + + PGPDigestCalculatorProvider getPGPDigestCalculatorProvider() throws PGPException; + + default PGPDigestCalculator getPGPDigestCalculator(HashAlgorithm algorithm) throws PGPException { + return getPGPDigestCalculator(algorithm.getAlgorithmId()); + } + + default PGPDigestCalculator getPGPDigestCalculator(int algorithm) throws PGPException { + return getPGPDigestCalculatorProvider().get(algorithm); + } + + PGPContentVerifierBuilderProvider getPGPContentVerifierBuilderProvider(); + + default PGPContentSignerBuilder getPGPContentSignerBuilder(PublicKeyAlgorithm keyAlgorithm, HashAlgorithm hashAlgorithm) { + return getPGPContentSignerBuilder(keyAlgorithm.getAlgorithmId(), hashAlgorithm.getAlgorithmId()); + } + + PGPContentSignerBuilder getPGPContentSignerBuilder(int keyAlgorithm, int hashAlgorithm); + + KeyFingerPrintCalculator getKeyFingerprintCalculator(); + + PBEDataDecryptorFactory getPBEDataDecryptorFactory(Passphrase passphrase) throws PGPException; + + PublicKeyDataDecryptorFactory getPublicKeyDataDecryptorFactory(PGPPrivateKey privateKey); + + PublicKeyKeyEncryptionMethodGenerator getPublicKeyKeyEncryptionMethodGenerator(PGPPublicKey key); + + PBEKeyEncryptionMethodGenerator getPBEKeyEncryptionMethodGenerator(Passphrase passphrase); + + default PGPDataEncryptorBuilder getPGPDataEncryptorBuilder(SymmetricKeyAlgorithm symmetricKeyAlgorithm) { + return getPGPDataEncryptorBuilder(symmetricKeyAlgorithm.getAlgorithmId()); + } + + PGPDataEncryptorBuilder getPGPDataEncryptorBuilder(int symmetricKeyAlgorithm); + + PGPKeyPair getPGPKeyPair(PublicKeyAlgorithm algorithm, KeyPair keyPair, Date creationDate) throws PGPException; + + PGPKeyPair getPGPKeyPair(PublicKeyAlgorithm algorithm, AsymmetricCipherKeyPair keyPair, Date creationDate) throws PGPException, NoSuchAlgorithmException, IOException, InvalidKeySpecException; + + PBESecretKeyEncryptor getPBESecretKeyEncryptor(SymmetricKeyAlgorithm encryptionAlgorithm, + HashAlgorithm hashAlgorithm, + int s2kCount, + Passphrase passphrase) throws PGPException; +} diff --git a/pgpainless-core/src/main/java/org/pgpainless/implementation/ImplementationFactory.java b/pgpainless-core/src/main/java/org/pgpainless/implementation/ImplementationFactory.java new file mode 100644 index 00000000..293f7bee --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/implementation/ImplementationFactory.java @@ -0,0 +1,15 @@ +package org.pgpainless.implementation; + +public class ImplementationFactory { + + private static CryptoEngineImplementation FACTORY_IMPLEMENTATION = new BcCryptoEngineImplementation(); + + public static void setFactoryImplementation(CryptoEngineImplementation implementation) { + FACTORY_IMPLEMENTATION = implementation; + } + + public static CryptoEngineImplementation getInstance() { + return FACTORY_IMPLEMENTATION; + } + +} diff --git a/pgpainless-core/src/main/java/org/pgpainless/implementation/JceCryptoEngineImplementation.java b/pgpainless-core/src/main/java/org/pgpainless/implementation/JceCryptoEngineImplementation.java new file mode 100644 index 00000000..b957d790 --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/implementation/JceCryptoEngineImplementation.java @@ -0,0 +1,162 @@ +package org.pgpainless.implementation; + +import java.io.IOException; +import java.security.KeyFactory; +import java.security.KeyPair; +import java.security.NoSuchAlgorithmException; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.PKCS8EncodedKeySpec; +import java.security.spec.X509EncodedKeySpec; +import java.util.Date; + +import org.bouncycastle.crypto.AsymmetricCipherKeyPair; +import org.bouncycastle.crypto.util.PrivateKeyInfoFactory; +import org.bouncycastle.crypto.util.SubjectPublicKeyInfoFactory; +import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.PGPKeyPair; +import org.bouncycastle.openpgp.PGPPrivateKey; +import org.bouncycastle.openpgp.PGPPublicKey; +import org.bouncycastle.openpgp.PGPSecretKey; +import org.bouncycastle.openpgp.operator.KeyFingerPrintCalculator; +import org.bouncycastle.openpgp.operator.PBEDataDecryptorFactory; +import org.bouncycastle.openpgp.operator.PBEKeyEncryptionMethodGenerator; +import org.bouncycastle.openpgp.operator.PBESecretKeyDecryptor; +import org.bouncycastle.openpgp.operator.PBESecretKeyEncryptor; +import org.bouncycastle.openpgp.operator.PGPContentSignerBuilder; +import org.bouncycastle.openpgp.operator.PGPContentVerifierBuilderProvider; +import org.bouncycastle.openpgp.operator.PGPDataEncryptorBuilder; +import org.bouncycastle.openpgp.operator.PGPDigestCalculator; +import org.bouncycastle.openpgp.operator.PGPDigestCalculatorProvider; +import org.bouncycastle.openpgp.operator.PublicKeyDataDecryptorFactory; +import org.bouncycastle.openpgp.operator.PublicKeyKeyEncryptionMethodGenerator; +import org.bouncycastle.openpgp.operator.jcajce.JcaKeyFingerprintCalculator; +import org.bouncycastle.openpgp.operator.jcajce.JcaPGPContentSignerBuilder; +import org.bouncycastle.openpgp.operator.jcajce.JcaPGPContentVerifierBuilderProvider; +import org.bouncycastle.openpgp.operator.jcajce.JcaPGPDigestCalculatorProviderBuilder; +import org.bouncycastle.openpgp.operator.jcajce.JcaPGPKeyPair; +import org.bouncycastle.openpgp.operator.jcajce.JcePBEDataDecryptorFactoryBuilder; +import org.bouncycastle.openpgp.operator.jcajce.JcePBEKeyEncryptionMethodGenerator; +import org.bouncycastle.openpgp.operator.jcajce.JcePBESecretKeyDecryptorBuilder; +import org.bouncycastle.openpgp.operator.jcajce.JcePBESecretKeyEncryptorBuilder; +import org.bouncycastle.openpgp.operator.jcajce.JcePGPDataEncryptorBuilder; +import org.bouncycastle.openpgp.operator.jcajce.JcePublicKeyDataDecryptorFactoryBuilder; +import org.bouncycastle.openpgp.operator.jcajce.JcePublicKeyKeyEncryptionMethodGenerator; +import org.pgpainless.algorithm.HashAlgorithm; +import org.pgpainless.algorithm.PublicKeyAlgorithm; +import org.pgpainless.algorithm.SymmetricKeyAlgorithm; +import org.pgpainless.provider.ProviderFactory; +import org.pgpainless.util.Passphrase; + +public class JceCryptoEngineImplementation implements CryptoEngineImplementation { + + @Override + public PBESecretKeyEncryptor getPBESecretKeyEncryptor(PGPSecretKey secretKey, Passphrase passphrase) + throws PGPException { + return new JcePBESecretKeyEncryptorBuilder(secretKey.getKeyEncryptionAlgorithm()) + .setProvider(ProviderFactory.getProvider()) + .build(passphrase.getChars()); + } + + @Override + public PBESecretKeyEncryptor getPBESecretKeyEncryptor(SymmetricKeyAlgorithm symmetricKeyAlgorithm, PGPDigestCalculator digestCalculator, Passphrase passphrase) { + return new JcePBESecretKeyEncryptorBuilder(symmetricKeyAlgorithm.getAlgorithmId(), digestCalculator) + .setProvider(ProviderFactory.getProvider()) + .build(passphrase.getChars()); + } + + @Override + public PBESecretKeyDecryptor getPBESecretKeyDecryptor(Passphrase passphrase) throws PGPException { + return new JcePBESecretKeyDecryptorBuilder(getPGPDigestCalculatorProvider()) + .setProvider(ProviderFactory.getProvider()) + .build(passphrase.getChars()); + } + + @Override + public PGPDigestCalculatorProvider getPGPDigestCalculatorProvider() + throws PGPException { + return new JcaPGPDigestCalculatorProviderBuilder() + .setProvider(ProviderFactory.getProvider()) + .build(); + } + + @Override + public PGPContentVerifierBuilderProvider getPGPContentVerifierBuilderProvider() { + return new JcaPGPContentVerifierBuilderProvider() + .setProvider(ProviderFactory.getProvider()); + } + + @Override + public PGPContentSignerBuilder getPGPContentSignerBuilder(int keyAlgorithm, int hashAlgorithm) { + return new JcaPGPContentSignerBuilder(keyAlgorithm, hashAlgorithm) + .setProvider(ProviderFactory.getProvider()); + } + + @Override + public KeyFingerPrintCalculator getKeyFingerprintCalculator() { + return new JcaKeyFingerprintCalculator() + .setProvider(ProviderFactory.getProvider()); + } + + @Override + public PBEDataDecryptorFactory getPBEDataDecryptorFactory(Passphrase passphrase) + throws PGPException { + return new JcePBEDataDecryptorFactoryBuilder(getPGPDigestCalculatorProvider()) + .setProvider(ProviderFactory.getProvider()) + .build(passphrase.getChars()); + } + + @Override + public PublicKeyDataDecryptorFactory getPublicKeyDataDecryptorFactory(PGPPrivateKey privateKey) { + return new JcePublicKeyDataDecryptorFactoryBuilder() + .setProvider(ProviderFactory.getProvider()) + .build(privateKey); + } + + @Override + public PublicKeyKeyEncryptionMethodGenerator getPublicKeyKeyEncryptionMethodGenerator(PGPPublicKey key) { + return new JcePublicKeyKeyEncryptionMethodGenerator(key) + .setProvider(ProviderFactory.getProvider()); + } + + @Override + public PBEKeyEncryptionMethodGenerator getPBEKeyEncryptionMethodGenerator(Passphrase passphrase) { + return new JcePBEKeyEncryptionMethodGenerator(passphrase.getChars()) + .setProvider(ProviderFactory.getProvider()); + } + + @Override + public PGPDataEncryptorBuilder getPGPDataEncryptorBuilder(int symmetricKeyAlgorithm) { + return new JcePGPDataEncryptorBuilder(symmetricKeyAlgorithm) + .setProvider(ProviderFactory.getProvider()); + } + + @Override + public PGPKeyPair getPGPKeyPair(PublicKeyAlgorithm algorithm, KeyPair keyPair, Date creationDate) throws PGPException { + return new JcaPGPKeyPair(algorithm.getAlgorithmId(), keyPair, creationDate); + } + + @Override + public PGPKeyPair getPGPKeyPair(PublicKeyAlgorithm algorithm, AsymmetricCipherKeyPair keyPair, Date creationDate) throws PGPException, NoSuchAlgorithmException, IOException, InvalidKeySpecException { + return new JcaPGPKeyPair(algorithm.getAlgorithmId(), bcToJceKeyPair(keyPair), creationDate); + } + + @Override + public PBESecretKeyEncryptor getPBESecretKeyEncryptor(SymmetricKeyAlgorithm encryptionAlgorithm, HashAlgorithm hashAlgorithm, int s2kCount, Passphrase passphrase) throws PGPException { + return new JcePBESecretKeyEncryptorBuilder( + encryptionAlgorithm.getAlgorithmId(), + getPGPDigestCalculator(hashAlgorithm), + s2kCount) + .setProvider(ProviderFactory.getProvider()) + .build(passphrase.getChars()); + } + + private KeyPair bcToJceKeyPair(AsymmetricCipherKeyPair keyPair) + throws NoSuchAlgorithmException, InvalidKeySpecException, IOException { + byte[] pkcs8Encoded = PrivateKeyInfoFactory.createPrivateKeyInfo(keyPair.getPrivate()).getEncoded(); + PKCS8EncodedKeySpec pkcs8KeySpec = new PKCS8EncodedKeySpec(pkcs8Encoded); + byte[] spkiEncoded = SubjectPublicKeyInfoFactory.createSubjectPublicKeyInfo(keyPair.getPublic()).getEncoded(); + X509EncodedKeySpec spkiKeySpec = new X509EncodedKeySpec(spkiEncoded); + KeyFactory keyFac = KeyFactory.getInstance("RSA"); + return new KeyPair(keyFac.generatePublic(spkiKeySpec), keyFac.generatePrivate(pkcs8KeySpec)); + } +} diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/generation/KeyRingBuilder.java b/pgpainless-core/src/main/java/org/pgpainless/key/generation/KeyRingBuilder.java index afe3c1c5..f446383f 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/key/generation/KeyRingBuilder.java +++ b/pgpainless-core/src/main/java/org/pgpainless/key/generation/KeyRingBuilder.java @@ -29,7 +29,6 @@ import java.util.List; import java.util.Set; import javax.annotation.Nonnull; -import org.bouncycastle.openpgp.PGPEncryptedData; import org.bouncycastle.openpgp.PGPException; import org.bouncycastle.openpgp.PGPKeyPair; import org.bouncycastle.openpgp.PGPKeyRingGenerator; @@ -44,14 +43,11 @@ import org.bouncycastle.openpgp.operator.PBESecretKeyDecryptor; import org.bouncycastle.openpgp.operator.PBESecretKeyEncryptor; import org.bouncycastle.openpgp.operator.PGPContentSignerBuilder; import org.bouncycastle.openpgp.operator.PGPDigestCalculator; -import org.bouncycastle.openpgp.operator.jcajce.JcaPGPContentSignerBuilder; -import org.bouncycastle.openpgp.operator.jcajce.JcaPGPDigestCalculatorProviderBuilder; -import org.bouncycastle.openpgp.operator.jcajce.JcaPGPKeyPair; -import org.bouncycastle.openpgp.operator.jcajce.JcePBESecretKeyDecryptorBuilder; -import org.bouncycastle.openpgp.operator.jcajce.JcePBESecretKeyEncryptorBuilder; import org.pgpainless.algorithm.HashAlgorithm; import org.pgpainless.algorithm.KeyFlag; import org.pgpainless.algorithm.SignatureType; +import org.pgpainless.algorithm.SymmetricKeyAlgorithm; +import org.pgpainless.implementation.ImplementationFactory; import org.pgpainless.key.generation.type.KeyType; import org.pgpainless.key.generation.type.ecc.EllipticCurve; import org.pgpainless.key.generation.type.rsa.RsaLength; @@ -331,33 +327,28 @@ public class KeyRingBuilder implements KeyRingBuilderInterface { } private PGPContentSignerBuilder buildContentSigner(PGPKeyPair certKey) { - return new JcaPGPContentSignerBuilder( - certKey.getPublicKey().getAlgorithm(), HashAlgorithm.SHA512.getAlgorithmId()) - .setProvider(ProviderFactory.getProvider()); + return ImplementationFactory.getInstance().getPGPContentSignerBuilder( + certKey.getPublicKey().getAlgorithm(), + HashAlgorithm.SHA512.getAlgorithmId()); } private PBESecretKeyEncryptor buildSecretKeyEncryptor() { PBESecretKeyEncryptor encryptor = passphrase == null || passphrase.isEmpty() ? null : // unencrypted key pair, otherwise AES-256 encrypted - new JcePBESecretKeyEncryptorBuilder(PGPEncryptedData.AES_256, digestCalculator) - .setProvider(ProviderFactory.getProvider()) - .build(passphrase.getChars()); + ImplementationFactory.getInstance().getPBESecretKeyEncryptor( + SymmetricKeyAlgorithm.AES_256, digestCalculator, passphrase); return encryptor; } private PBESecretKeyDecryptor buildSecretKeyDecryptor() throws PGPException { PBESecretKeyDecryptor decryptor = passphrase == null || passphrase.isEmpty() ? null : - new JcePBESecretKeyDecryptorBuilder() - .build(passphrase.getChars()); + ImplementationFactory.getInstance().getPBESecretKeyDecryptor(passphrase); return decryptor; } private PGPDigestCalculator buildDigestCalculator() throws PGPException { - return new JcaPGPDigestCalculatorProviderBuilder() - .setProvider(ProviderFactory.getProvider()) - .build() - .get(HashAlgorithm.SHA1.getAlgorithmId()); + return ImplementationFactory.getInstance().getPGPDigestCalculator(HashAlgorithm.SHA1); } } } @@ -373,8 +364,7 @@ public class KeyRingBuilder implements KeyRingBuilderInterface { KeyPair keyPair = certKeyGenerator.generateKeyPair(); // Form PGP key pair - PGPKeyPair pgpKeyPair = new JcaPGPKeyPair(type.getAlgorithm().getAlgorithmId(), - keyPair, new Date()); + PGPKeyPair pgpKeyPair = ImplementationFactory.getInstance().getPGPKeyPair(type.getAlgorithm(), keyPair, new Date()); return pgpKeyPair; } diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/modification/secretkeyring/SecretKeyRingEditor.java b/pgpainless-core/src/main/java/org/pgpainless/key/modification/secretkeyring/SecretKeyRingEditor.java index 52eccb0e..b9a94686 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/key/modification/secretkeyring/SecretKeyRingEditor.java +++ b/pgpainless-core/src/main/java/org/pgpainless/key/modification/secretkeyring/SecretKeyRingEditor.java @@ -45,12 +45,10 @@ import org.bouncycastle.openpgp.operator.PBESecretKeyDecryptor; import org.bouncycastle.openpgp.operator.PBESecretKeyEncryptor; import org.bouncycastle.openpgp.operator.PGPContentSignerBuilder; import org.bouncycastle.openpgp.operator.PGPDigestCalculator; -import org.bouncycastle.openpgp.operator.bc.BcPBESecretKeyEncryptorBuilder; -import org.bouncycastle.openpgp.operator.bc.BcPGPContentSignerBuilder; -import org.bouncycastle.openpgp.operator.bc.BcPGPDigestCalculatorProvider; import org.pgpainless.algorithm.HashAlgorithm; import org.pgpainless.algorithm.SignatureType; import org.pgpainless.algorithm.SymmetricKeyAlgorithm; +import org.pgpainless.implementation.ImplementationFactory; import org.pgpainless.key.OpenPgpV4Fingerprint; import org.pgpainless.key.generation.KeyRingBuilder; import org.pgpainless.key.generation.KeySpec; @@ -202,10 +200,13 @@ public class SecretKeyRingEditor implements SecretKeyRingEditorInterface { PBESecretKeyDecryptor ringDecryptor = keyRingProtector.getDecryptor(primaryKey.getKeyID()); PBESecretKeyEncryptor subKeyEncryptor = subKeyProtector.getEncryptor(secretSubKey.getKeyID()); - PGPDigestCalculator digestCalculator = new BcPGPDigestCalculatorProvider() - .get(defaultDigestHashAlgorithm.getAlgorithmId()); - PGPContentSignerBuilder contentSignerBuilder = new BcPGPContentSignerBuilder( - primaryKey.getAlgorithm(), HashAlgorithm.SHA256.getAlgorithmId()); + PGPDigestCalculator digestCalculator = + ImplementationFactory.getInstance().getPGPDigestCalculator(defaultDigestHashAlgorithm); + PGPContentSignerBuilder contentSignerBuilder = ImplementationFactory.getInstance() + .getPGPContentSignerBuilder( + primaryKey.getAlgorithm(), + HashAlgorithm.SHA256.getAlgorithmId() // TODO: Why SHA256? + ); PGPPrivateKey privateSubKey = unlockSecretKey(secretSubKey, subKeyProtector); PGPKeyPair subKeyPair = new PGPKeyPair(secretSubKey.getPublicKey(), privateSubKey); @@ -222,12 +223,11 @@ public class SecretKeyRingEditor implements SecretKeyRingEditorInterface { private PGPSecretKey generateSubKey(@Nonnull KeySpec keySpec, @Nonnull Passphrase subKeyPassphrase) throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException { - PGPDigestCalculator checksumCalculator = new BcPGPDigestCalculatorProvider() - .get(defaultDigestHashAlgorithm.getAlgorithmId()); + PGPDigestCalculator checksumCalculator = ImplementationFactory.getInstance() + .getPGPDigestCalculator(defaultDigestHashAlgorithm); PBESecretKeyEncryptor subKeyEncryptor = subKeyPassphrase.isEmpty() ? null : - new BcPBESecretKeyEncryptorBuilder(SymmetricKeyAlgorithm.AES_256.getAlgorithmId()) - .build(subKeyPassphrase.getChars()); + ImplementationFactory.getInstance().getPBESecretKeyEncryptor(SymmetricKeyAlgorithm.AES_256, subKeyPassphrase); PGPKeyPair keyPair = KeyRingBuilder.generateKeyPair(keySpec); PGPSecretKey secretKey = new PGPSecretKey(keyPair.getPrivateKey(), keyPair.getPublicKey(), @@ -530,8 +530,8 @@ public class SecretKeyRingEditor implements SecretKeyRingEditorInterface { // TODO: Move to utility class private PGPSecretKey lockPrivateKey(PGPPrivateKey privateKey, PGPPublicKey publicKey, SecretKeyRingProtector protector) throws PGPException { - PGPDigestCalculator checksumCalculator = new BcPGPDigestCalculatorProvider() - .get(defaultDigestHashAlgorithm.getAlgorithmId()); + PGPDigestCalculator checksumCalculator = ImplementationFactory.getInstance() + .getPGPDigestCalculator(defaultDigestHashAlgorithm); PBESecretKeyEncryptor encryptor = protector.getEncryptor(publicKey.getKeyID()); PGPSecretKey secretKey = new PGPSecretKey(privateKey, publicKey, checksumCalculator, publicKey.isMasterKey(), encryptor); return secretKey; diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/parsing/KeyRingReader.java b/pgpainless-core/src/main/java/org/pgpainless/key/parsing/KeyRingReader.java index 7b0297c7..6f3a1c2c 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/key/parsing/KeyRingReader.java +++ b/pgpainless-core/src/main/java/org/pgpainless/key/parsing/KeyRingReader.java @@ -27,7 +27,7 @@ import org.bouncycastle.openpgp.PGPPublicKeyRingCollection; import org.bouncycastle.openpgp.PGPSecretKeyRing; import org.bouncycastle.openpgp.PGPSecretKeyRingCollection; import org.bouncycastle.openpgp.PGPUtil; -import org.bouncycastle.openpgp.operator.bc.BcKeyFingerprintCalculator; +import org.pgpainless.implementation.ImplementationFactory; public class KeyRingReader { @@ -90,27 +90,27 @@ public class KeyRingReader { public static PGPPublicKeyRing readPublicKeyRing(@Nonnull InputStream inputStream) throws IOException { return new PGPPublicKeyRing( PGPUtil.getDecoderStream(inputStream), - new BcKeyFingerprintCalculator()); + ImplementationFactory.getInstance().getKeyFingerprintCalculator()); } public static PGPPublicKeyRingCollection readPublicKeyRingCollection(@Nonnull InputStream inputStream) throws IOException, PGPException { return new PGPPublicKeyRingCollection( PGPUtil.getDecoderStream(inputStream), - new BcKeyFingerprintCalculator()); + ImplementationFactory.getInstance().getKeyFingerprintCalculator()); } public static PGPSecretKeyRing readSecretKeyRing(@Nonnull InputStream inputStream) throws IOException, PGPException { return new PGPSecretKeyRing( PGPUtil.getDecoderStream(inputStream), - new BcKeyFingerprintCalculator()); + ImplementationFactory.getInstance().getKeyFingerprintCalculator()); } public static PGPSecretKeyRingCollection readSecretKeyRingCollection(@Nonnull InputStream inputStream) throws IOException, PGPException { return new PGPSecretKeyRingCollection( PGPUtil.getDecoderStream(inputStream), - new BcKeyFingerprintCalculator()); + ImplementationFactory.getInstance().getKeyFingerprintCalculator()); } private static void validateStreamsNotBothNull(InputStream publicIn, InputStream secretIn) { diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/protection/CallbackBasedKeyringProtector.java b/pgpainless-core/src/main/java/org/pgpainless/key/protection/CallbackBasedKeyringProtector.java new file mode 100644 index 00000000..4e8ad59b --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/key/protection/CallbackBasedKeyringProtector.java @@ -0,0 +1,52 @@ +package org.pgpainless.key.protection; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.PGPSecretKey; +import org.bouncycastle.openpgp.operator.PBESecretKeyDecryptor; +import org.bouncycastle.openpgp.operator.PBESecretKeyEncryptor; +import org.pgpainless.implementation.ImplementationFactory; +import org.pgpainless.util.Passphrase; + +public class CallbackBasedKeyringProtector implements SecretKeyRingProtector2 { + + private final Map passphraseCache = new ConcurrentHashMap<>(); + private final Callback callback; + + public CallbackBasedKeyringProtector(Callback callback) { + if (callback == null) { + throw new NullPointerException("Callback MUST NOT be null."); + } + this.callback = callback; + } + + @Override + public PBESecretKeyDecryptor getDecryptor(PGPSecretKey key) throws PGPException { + Passphrase passphrase = lookupPassphraseInCache(key); + if (passphrase != null) { + passphrase = callback.getPassphraseFor(key); + passphraseCache.put(key.getKeyID(), passphrase); + } + return ImplementationFactory.getInstance().getPBESecretKeyDecryptor(passphrase); + } + + @Override + public PBESecretKeyEncryptor getEncryptor(PGPSecretKey key) throws PGPException { + Passphrase passphrase = lookupPassphraseInCache(key); + if (passphrase != null) { + passphrase = callback.getPassphraseFor(key); + passphraseCache.put(key.getKeyID(), passphrase); + } + return ImplementationFactory.getInstance().getPBESecretKeyEncryptor(key, passphrase); + } + + private Passphrase lookupPassphraseInCache(PGPSecretKey key) { + return passphraseCache.get(key.getKeyID()); + } + + public interface Callback { + Passphrase getPassphraseFor(PGPSecretKey secretKey); + } +} diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/protection/PassphraseMapKeyRingProtector.java b/pgpainless-core/src/main/java/org/pgpainless/key/protection/PassphraseMapKeyRingProtector.java index 385e82fb..ad348a7e 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/key/protection/PassphraseMapKeyRingProtector.java +++ b/pgpainless-core/src/main/java/org/pgpainless/key/protection/PassphraseMapKeyRingProtector.java @@ -15,10 +15,10 @@ */ package org.pgpainless.key.protection; -import javax.annotation.Nonnull; -import javax.annotation.Nullable; import java.util.HashMap; import java.util.Map; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; import org.bouncycastle.openpgp.PGPException; import org.bouncycastle.openpgp.operator.PBESecretKeyDecryptor; @@ -69,7 +69,7 @@ public class PassphraseMapKeyRingProtector implements SecretKeyRingProtector, Se @Override @Nullable - public Passphrase getPassphraseFor(@Nonnull Long keyId) { + public Passphrase getPassphraseFor(Long keyId) { Passphrase passphrase = cache.get(keyId); if (passphrase == null || !passphrase.isValid()) { passphrase = provider.getPassphraseFor(keyId); @@ -82,7 +82,7 @@ public class PassphraseMapKeyRingProtector implements SecretKeyRingProtector, Se @Override @Nullable - public PBESecretKeyDecryptor getDecryptor(@Nonnull Long keyId) { + public PBESecretKeyDecryptor getDecryptor(@Nonnull Long keyId) throws PGPException { return protector.getDecryptor(keyId); } diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/protection/PasswordBasedSecretKeyRingProtector.java b/pgpainless-core/src/main/java/org/pgpainless/key/protection/PasswordBasedSecretKeyRingProtector.java index 7328b77b..0bec8b00 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/key/protection/PasswordBasedSecretKeyRingProtector.java +++ b/pgpainless-core/src/main/java/org/pgpainless/key/protection/PasswordBasedSecretKeyRingProtector.java @@ -23,10 +23,7 @@ import org.bouncycastle.openpgp.PGPKeyRing; import org.bouncycastle.openpgp.PGPSecretKey; import org.bouncycastle.openpgp.operator.PBESecretKeyDecryptor; import org.bouncycastle.openpgp.operator.PBESecretKeyEncryptor; -import org.bouncycastle.openpgp.operator.PGPDigestCalculatorProvider; -import org.bouncycastle.openpgp.operator.bc.BcPBESecretKeyDecryptorBuilder; -import org.bouncycastle.openpgp.operator.bc.BcPBESecretKeyEncryptorBuilder; -import org.bouncycastle.openpgp.operator.bc.BcPGPDigestCalculatorProvider; +import org.pgpainless.implementation.ImplementationFactory; import org.pgpainless.key.protection.passphrase_provider.SecretKeyPassphraseProvider; import org.pgpainless.util.Passphrase; @@ -36,8 +33,6 @@ import org.pgpainless.util.Passphrase; */ public class PasswordBasedSecretKeyRingProtector implements SecretKeyRingProtector { - private static final PGPDigestCalculatorProvider calculatorProvider = new BcPGPDigestCalculatorProvider(); - protected final KeyRingProtectionSettings protectionSettings; protected final SecretKeyPassphraseProvider passphraseProvider; @@ -86,11 +81,10 @@ public class PasswordBasedSecretKeyRingProtector implements SecretKeyRingProtect @Override @Nullable - public PBESecretKeyDecryptor getDecryptor(Long keyId) { + public PBESecretKeyDecryptor getDecryptor(Long keyId) throws PGPException { Passphrase passphrase = passphraseProvider.getPassphraseFor(keyId); return passphrase == null ? null : - new BcPBESecretKeyDecryptorBuilder(calculatorProvider) - .build(passphrase.getChars()); + ImplementationFactory.getInstance().getPBESecretKeyDecryptor(passphrase); } @Override @@ -98,10 +92,10 @@ public class PasswordBasedSecretKeyRingProtector implements SecretKeyRingProtect public PBESecretKeyEncryptor getEncryptor(Long keyId) throws PGPException { Passphrase passphrase = passphraseProvider.getPassphraseFor(keyId); return passphrase == null ? null : - new BcPBESecretKeyEncryptorBuilder( - protectionSettings.getEncryptionAlgorithm().getAlgorithmId(), - calculatorProvider.get(protectionSettings.getHashAlgorithm().getAlgorithmId()), - protectionSettings.getS2kCount()) - .build(passphrase.getChars()); + ImplementationFactory.getInstance().getPBESecretKeyEncryptor( + protectionSettings.getEncryptionAlgorithm(), + protectionSettings.getHashAlgorithm(), + protectionSettings.getS2kCount(), + passphrase); } } diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/protection/SecretKeyRingProtector.java b/pgpainless-core/src/main/java/org/pgpainless/key/protection/SecretKeyRingProtector.java index 4d27ed02..7a48d740 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/key/protection/SecretKeyRingProtector.java +++ b/pgpainless-core/src/main/java/org/pgpainless/key/protection/SecretKeyRingProtector.java @@ -25,6 +25,9 @@ import org.bouncycastle.openpgp.operator.PBESecretKeyDecryptor; import org.bouncycastle.openpgp.operator.PBESecretKeyEncryptor; import org.pgpainless.util.Passphrase; +/** + * @deprecated use {@link SecretKeyRingProtector2} instead. + */ public interface SecretKeyRingProtector { /** @@ -34,7 +37,7 @@ public interface SecretKeyRingProtector { * @param keyId id of the key * @return decryptor for the key */ - @Nullable PBESecretKeyDecryptor getDecryptor(Long keyId); + @Nullable PBESecretKeyDecryptor getDecryptor(Long keyId) throws PGPException; /** * Return an encryptor for the key of id {@code keyId}. diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/protection/SecretKeyRingProtector2.java b/pgpainless-core/src/main/java/org/pgpainless/key/protection/SecretKeyRingProtector2.java new file mode 100644 index 00000000..61c68f19 --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/key/protection/SecretKeyRingProtector2.java @@ -0,0 +1,13 @@ +package org.pgpainless.key.protection; + +import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.PGPSecretKey; +import org.bouncycastle.openpgp.operator.PBESecretKeyDecryptor; +import org.bouncycastle.openpgp.operator.PBESecretKeyEncryptor; + +public interface SecretKeyRingProtector2 { + + PBESecretKeyDecryptor getDecryptor(PGPSecretKey key) throws PGPException; + + PBESecretKeyEncryptor getEncryptor(PGPSecretKey key) throws PGPException; +} diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/protection/SecretKeyRingProtectorAdapter.java b/pgpainless-core/src/main/java/org/pgpainless/key/protection/SecretKeyRingProtectorAdapter.java new file mode 100644 index 00000000..4252231f --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/key/protection/SecretKeyRingProtectorAdapter.java @@ -0,0 +1,19 @@ +package org.pgpainless.key.protection; + +import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.PGPSecretKey; +import org.bouncycastle.openpgp.operator.PBESecretKeyDecryptor; +import org.bouncycastle.openpgp.operator.PBESecretKeyEncryptor; + +public interface SecretKeyRingProtectorAdapter extends SecretKeyRingProtector, SecretKeyRingProtector2 { + + @Override + default PBESecretKeyDecryptor getDecryptor(PGPSecretKey key) throws PGPException { + return getDecryptor(key.getKeyID()); + } + + @Override + default PBESecretKeyEncryptor getEncryptor(PGPSecretKey key) throws PGPException { + return getEncryptor(key.getKeyID()); + } +} diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/protection/passphrase_provider/MapBasedPassphraseProvider.java b/pgpainless-core/src/main/java/org/pgpainless/key/protection/passphrase_provider/MapBasedPassphraseProvider.java index 24ea569d..a1e1d865 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/key/protection/passphrase_provider/MapBasedPassphraseProvider.java +++ b/pgpainless-core/src/main/java/org/pgpainless/key/protection/passphrase_provider/MapBasedPassphraseProvider.java @@ -18,6 +18,7 @@ package org.pgpainless.key.protection.passphrase_provider; import java.util.Map; import javax.annotation.Nullable; +import org.bouncycastle.openpgp.PGPSecretKey; import org.pgpainless.util.Passphrase; /** diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/protection/passphrase_provider/SecretKeyPassphraseProvider.java b/pgpainless-core/src/main/java/org/pgpainless/key/protection/passphrase_provider/SecretKeyPassphraseProvider.java index aa5d43ef..1b81ef93 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/key/protection/passphrase_provider/SecretKeyPassphraseProvider.java +++ b/pgpainless-core/src/main/java/org/pgpainless/key/protection/passphrase_provider/SecretKeyPassphraseProvider.java @@ -17,6 +17,7 @@ package org.pgpainless.key.protection.passphrase_provider; import javax.annotation.Nullable; +import org.bouncycastle.openpgp.PGPSecretKey; import org.pgpainless.util.Passphrase; /** @@ -24,12 +25,15 @@ import org.pgpainless.util.Passphrase; */ public interface SecretKeyPassphraseProvider { + @Nullable default Passphrase getPassphraseFor(PGPSecretKey secretKey) { + return getPassphraseFor(secretKey.getKeyID()); + } /** * Return a passphrase for the given key. If no record has been found, return null. * Note: In case of an unprotected secret key, this method must may not return null, but a {@link Passphrase} with * a content of null. * - * @param keyId id of the key + * @param keyId if of the secret key * @return passphrase or null, if no passphrase record has been found. */ @Nullable Passphrase getPassphraseFor(Long keyId); diff --git a/pgpainless-core/src/main/java/org/pgpainless/util/BCUtil.java b/pgpainless-core/src/main/java/org/pgpainless/util/BCUtil.java index 1d29f841..b68f63b3 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/util/BCUtil.java +++ b/pgpainless-core/src/main/java/org/pgpainless/util/BCUtil.java @@ -15,7 +15,6 @@ */ package org.pgpainless.util; -import javax.annotation.Nonnull; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; @@ -26,6 +25,7 @@ import java.util.Iterator; import java.util.Set; import java.util.logging.Level; import java.util.logging.Logger; +import javax.annotation.Nonnull; import org.bouncycastle.openpgp.PGPException; import org.bouncycastle.openpgp.PGPKeyRing; @@ -38,13 +38,14 @@ import org.bouncycastle.openpgp.PGPSecretKeyRingCollection; import org.bouncycastle.openpgp.PGPSignature; import org.bouncycastle.openpgp.PGPSignatureSubpacketVector; import org.bouncycastle.openpgp.PGPUtil; -import org.bouncycastle.openpgp.operator.bc.BcKeyFingerprintCalculator; +import org.bouncycastle.openpgp.operator.KeyFingerPrintCalculator; import org.bouncycastle.util.io.Streams; import org.pgpainless.algorithm.KeyFlag; +import org.pgpainless.implementation.ImplementationFactory; import org.pgpainless.key.selection.key.PublicKeySelectionStrategy; -import org.pgpainless.key.selection.key.util.And; import org.pgpainless.key.selection.key.impl.NoRevocation; import org.pgpainless.key.selection.key.impl.SignedByMasterKey; +import org.pgpainless.key.selection.key.util.And; public class BCUtil { @@ -72,8 +73,9 @@ public class BCUtil { publicKey.encode(buffer, false); } } + KeyFingerPrintCalculator fingerprintCalculator = ImplementationFactory.getInstance().getKeyFingerprintCalculator(); - return new PGPPublicKeyRing(buffer.toByteArray(), new BcKeyFingerprintCalculator()); + return new PGPPublicKeyRing(buffer.toByteArray(), fingerprintCalculator); } /* diff --git a/pgpainless-core/src/test/java/org/pgpainless/key/protection/PassphraseProtectedKeyTest.java b/pgpainless-core/src/test/java/org/pgpainless/key/protection/PassphraseProtectedKeyTest.java index af97d100..ac317740 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/key/protection/PassphraseProtectedKeyTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/key/protection/PassphraseProtectedKeyTest.java @@ -21,6 +21,7 @@ import static org.junit.jupiter.api.Assertions.assertNull; import javax.annotation.Nullable; import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.PGPSecretKey; import org.junit.jupiter.api.Test; import org.pgpainless.key.TestKeys; import org.pgpainless.key.protection.passphrase_provider.SecretKeyPassphraseProvider; @@ -37,7 +38,7 @@ public class PassphraseProtectedKeyTest { @Nullable @Override public Passphrase getPassphraseFor(Long keyId) { - if (keyId == TestKeys.CRYPTIE_KEY_ID) { + if (keyId.equals(TestKeys.CRYPTIE_KEY_ID)) { return new Passphrase(TestKeys.CRYPTIE_PASSWORD.toCharArray()); } else { return null; diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/SopKeyUtil.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/SopKeyUtil.java new file mode 100644 index 00000000..551f4532 --- /dev/null +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/SopKeyUtil.java @@ -0,0 +1,29 @@ +package org.pgpainless.sop; + +import static org.pgpainless.sop.Print.err_ln; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.PGPSecretKeyRing; +import org.pgpainless.PGPainless; + +public class SopKeyUtil { + + public static List loadKeysFromFiles(File... files) throws IOException, PGPException { + List secretKeyRings = new ArrayList<>(); + for (File file : files) { + try(FileInputStream in = new FileInputStream(file)) { + secretKeyRings.add(PGPainless.readKeyRing().secretKeyRing(in)); + } catch (PGPException | IOException e) { + err_ln("Could not load secret key " + file.getName() + ": " + e.getMessage()); + throw e; + } + } + return secretKeyRings; + } +} diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/commands/Decrypt.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/commands/Decrypt.java index 594967e4..bb5022ea 100644 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/commands/Decrypt.java +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/commands/Decrypt.java @@ -15,11 +15,19 @@ */ package org.pgpainless.sop.commands; +import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.PGPSecretKey; +import org.bouncycastle.openpgp.PGPSecretKeyRing; +import org.bouncycastle.openpgp.PGPSecretKeyRingCollection; +import org.pgpainless.PGPainless; import picocli.CommandLine; import java.io.File; +import java.io.IOException; +import java.util.List; import static org.pgpainless.sop.Print.err_ln; +import static org.pgpainless.sop.SopKeyUtil.loadKeysFromFiles; @CommandLine.Command(name = "decrypt", description = "Decrypt a message from standard input") @@ -79,5 +87,26 @@ public class Decrypt implements Runnable { err_ln("To enable signature verification, both --verify-out and at least one --verify-with argument must be supplied."); System.exit(23); } + + if (sessionKeyOut != null || withSessionKey != null) { + err_ln("session key in and out are not yet supported."); + System.exit(1); + } + + PGPSecretKeyRingCollection secretKeys; + try { + List secretKeyRings = loadKeysFromFiles(keys); + secretKeys = new PGPSecretKeyRingCollection(secretKeyRings); + } catch (PGPException | IOException e) { + err_ln(e.getMessage()); + System.exit(1); + return; + } + + + + PGPainless.decryptAndOrVerify() + .onInputStream(System.in) + .decryptWith(secretKeys); } } From 82014ef6e5aad7cc9b5298ad13d160b7400359fb Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Sun, 3 Jan 2021 15:52:18 +0100 Subject: [PATCH 24/25] Fix checkstyle issues --- .../implementation/package-info.java | 19 +++++++++++++++++++ .../CallbackBasedKeyringProtector.java | 15 +++++++++++++++ .../protection/SecretKeyRingProtector.java | 2 ++ .../protection/SecretKeyRingProtector2.java | 15 +++++++++++++++ .../SecretKeyRingProtectorAdapter.java | 15 +++++++++++++++ .../MapBasedPassphraseProvider.java | 1 - .../PassphraseProtectedKeyTest.java | 1 - .../java/org/pgpainless/sop/SopKeyUtil.java | 17 ++++++++++++++++- .../org/pgpainless/sop/commands/Decrypt.java | 1 - 9 files changed, 82 insertions(+), 4 deletions(-) create mode 100644 pgpainless-core/src/main/java/org/pgpainless/implementation/package-info.java diff --git a/pgpainless-core/src/main/java/org/pgpainless/implementation/package-info.java b/pgpainless-core/src/main/java/org/pgpainless/implementation/package-info.java new file mode 100644 index 00000000..b3e8787e --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/implementation/package-info.java @@ -0,0 +1,19 @@ +/* + * Copyright 2020 Paul Schaub. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/** + * Implementation factory classes to be able to switch out the underlying crypto engine implementation. + */ +package org.pgpainless.implementation; diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/protection/CallbackBasedKeyringProtector.java b/pgpainless-core/src/main/java/org/pgpainless/key/protection/CallbackBasedKeyringProtector.java index 4e8ad59b..75677bbe 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/key/protection/CallbackBasedKeyringProtector.java +++ b/pgpainless-core/src/main/java/org/pgpainless/key/protection/CallbackBasedKeyringProtector.java @@ -1,3 +1,18 @@ +/* + * Copyright 2020 Paul Schaub. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package org.pgpainless.key.protection; import java.util.Map; diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/protection/SecretKeyRingProtector.java b/pgpainless-core/src/main/java/org/pgpainless/key/protection/SecretKeyRingProtector.java index 7a48d740..f75bff25 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/key/protection/SecretKeyRingProtector.java +++ b/pgpainless-core/src/main/java/org/pgpainless/key/protection/SecretKeyRingProtector.java @@ -26,6 +26,8 @@ import org.bouncycastle.openpgp.operator.PBESecretKeyEncryptor; import org.pgpainless.util.Passphrase; /** + * Interface that is used to provide secret key ring encryptors and decryptors. + * * @deprecated use {@link SecretKeyRingProtector2} instead. */ public interface SecretKeyRingProtector { diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/protection/SecretKeyRingProtector2.java b/pgpainless-core/src/main/java/org/pgpainless/key/protection/SecretKeyRingProtector2.java index 61c68f19..f215641a 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/key/protection/SecretKeyRingProtector2.java +++ b/pgpainless-core/src/main/java/org/pgpainless/key/protection/SecretKeyRingProtector2.java @@ -1,3 +1,18 @@ +/* + * Copyright 2020 Paul Schaub. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package org.pgpainless.key.protection; import org.bouncycastle.openpgp.PGPException; diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/protection/SecretKeyRingProtectorAdapter.java b/pgpainless-core/src/main/java/org/pgpainless/key/protection/SecretKeyRingProtectorAdapter.java index 4252231f..b9e46896 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/key/protection/SecretKeyRingProtectorAdapter.java +++ b/pgpainless-core/src/main/java/org/pgpainless/key/protection/SecretKeyRingProtectorAdapter.java @@ -1,3 +1,18 @@ +/* + * Copyright 2020 Paul Schaub. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package org.pgpainless.key.protection; import org.bouncycastle.openpgp.PGPException; diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/protection/passphrase_provider/MapBasedPassphraseProvider.java b/pgpainless-core/src/main/java/org/pgpainless/key/protection/passphrase_provider/MapBasedPassphraseProvider.java index a1e1d865..24ea569d 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/key/protection/passphrase_provider/MapBasedPassphraseProvider.java +++ b/pgpainless-core/src/main/java/org/pgpainless/key/protection/passphrase_provider/MapBasedPassphraseProvider.java @@ -18,7 +18,6 @@ package org.pgpainless.key.protection.passphrase_provider; import java.util.Map; import javax.annotation.Nullable; -import org.bouncycastle.openpgp.PGPSecretKey; import org.pgpainless.util.Passphrase; /** diff --git a/pgpainless-core/src/test/java/org/pgpainless/key/protection/PassphraseProtectedKeyTest.java b/pgpainless-core/src/test/java/org/pgpainless/key/protection/PassphraseProtectedKeyTest.java index ac317740..437ef12f 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/key/protection/PassphraseProtectedKeyTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/key/protection/PassphraseProtectedKeyTest.java @@ -21,7 +21,6 @@ import static org.junit.jupiter.api.Assertions.assertNull; import javax.annotation.Nullable; import org.bouncycastle.openpgp.PGPException; -import org.bouncycastle.openpgp.PGPSecretKey; import org.junit.jupiter.api.Test; import org.pgpainless.key.TestKeys; import org.pgpainless.key.protection.passphrase_provider.SecretKeyPassphraseProvider; diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/SopKeyUtil.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/SopKeyUtil.java index 551f4532..082bdc62 100644 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/SopKeyUtil.java +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/SopKeyUtil.java @@ -1,3 +1,18 @@ +/* + * Copyright 2020 Paul Schaub. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package org.pgpainless.sop; import static org.pgpainless.sop.Print.err_ln; @@ -17,7 +32,7 @@ public class SopKeyUtil { public static List loadKeysFromFiles(File... files) throws IOException, PGPException { List secretKeyRings = new ArrayList<>(); for (File file : files) { - try(FileInputStream in = new FileInputStream(file)) { + try (FileInputStream in = new FileInputStream(file)) { secretKeyRings.add(PGPainless.readKeyRing().secretKeyRing(in)); } catch (PGPException | IOException e) { err_ln("Could not load secret key " + file.getName() + ": " + e.getMessage()); diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/commands/Decrypt.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/commands/Decrypt.java index bb5022ea..4be22ebb 100644 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/commands/Decrypt.java +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/commands/Decrypt.java @@ -16,7 +16,6 @@ package org.pgpainless.sop.commands; import org.bouncycastle.openpgp.PGPException; -import org.bouncycastle.openpgp.PGPSecretKey; import org.bouncycastle.openpgp.PGPSecretKeyRing; import org.bouncycastle.openpgp.PGPSecretKeyRingCollection; import org.pgpainless.PGPainless; From d014c00aaac6413d8373b7dee0dc4e82079d7cf7 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Sun, 3 Jan 2021 15:52:33 +0100 Subject: [PATCH 25/25] Refactor implementationfactory --- ...tion.java => BcImplementationFactory.java} | 17 ++- .../CryptoEngineImplementation.java | 91 ------------ .../implementation/ImplementationFactory.java | 134 +++++++++++++++++- ...ion.java => JceImplementationFactory.java} | 32 ++--- 4 files changed, 162 insertions(+), 112 deletions(-) rename pgpainless-core/src/main/java/org/pgpainless/implementation/{BcCryptoEngineImplementation.java => BcImplementationFactory.java} (91%) delete mode 100644 pgpainless-core/src/main/java/org/pgpainless/implementation/CryptoEngineImplementation.java rename pgpainless-core/src/main/java/org/pgpainless/implementation/{JceCryptoEngineImplementation.java => JceImplementationFactory.java} (92%) diff --git a/pgpainless-core/src/main/java/org/pgpainless/implementation/BcCryptoEngineImplementation.java b/pgpainless-core/src/main/java/org/pgpainless/implementation/BcImplementationFactory.java similarity index 91% rename from pgpainless-core/src/main/java/org/pgpainless/implementation/BcCryptoEngineImplementation.java rename to pgpainless-core/src/main/java/org/pgpainless/implementation/BcImplementationFactory.java index 66829d07..7b75f422 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/implementation/BcCryptoEngineImplementation.java +++ b/pgpainless-core/src/main/java/org/pgpainless/implementation/BcImplementationFactory.java @@ -1,3 +1,18 @@ +/* + * Copyright 2020 Paul Schaub. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package org.pgpainless.implementation; import java.security.KeyPair; @@ -40,7 +55,7 @@ import org.pgpainless.algorithm.PublicKeyAlgorithm; import org.pgpainless.algorithm.SymmetricKeyAlgorithm; import org.pgpainless.util.Passphrase; -public class BcCryptoEngineImplementation implements CryptoEngineImplementation { +public class BcImplementationFactory extends ImplementationFactory { @Override public PBESecretKeyEncryptor getPBESecretKeyEncryptor(PGPSecretKey secretKey, Passphrase passphrase) diff --git a/pgpainless-core/src/main/java/org/pgpainless/implementation/CryptoEngineImplementation.java b/pgpainless-core/src/main/java/org/pgpainless/implementation/CryptoEngineImplementation.java deleted file mode 100644 index 154abb65..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/implementation/CryptoEngineImplementation.java +++ /dev/null @@ -1,91 +0,0 @@ -package org.pgpainless.implementation; - -import java.io.IOException; -import java.security.KeyPair; -import java.security.NoSuchAlgorithmException; -import java.security.spec.InvalidKeySpecException; -import java.util.Date; - -import org.bouncycastle.crypto.AsymmetricCipherKeyPair; -import org.bouncycastle.openpgp.PGPException; -import org.bouncycastle.openpgp.PGPKeyPair; -import org.bouncycastle.openpgp.PGPPrivateKey; -import org.bouncycastle.openpgp.PGPPublicKey; -import org.bouncycastle.openpgp.PGPSecretKey; -import org.bouncycastle.openpgp.operator.KeyFingerPrintCalculator; -import org.bouncycastle.openpgp.operator.PBEDataDecryptorFactory; -import org.bouncycastle.openpgp.operator.PBEKeyEncryptionMethodGenerator; -import org.bouncycastle.openpgp.operator.PBESecretKeyDecryptor; -import org.bouncycastle.openpgp.operator.PBESecretKeyEncryptor; -import org.bouncycastle.openpgp.operator.PGPContentSignerBuilder; -import org.bouncycastle.openpgp.operator.PGPContentVerifierBuilderProvider; -import org.bouncycastle.openpgp.operator.PGPDataEncryptorBuilder; -import org.bouncycastle.openpgp.operator.PGPDigestCalculator; -import org.bouncycastle.openpgp.operator.PGPDigestCalculatorProvider; -import org.bouncycastle.openpgp.operator.PublicKeyDataDecryptorFactory; -import org.bouncycastle.openpgp.operator.PublicKeyKeyEncryptionMethodGenerator; -import org.pgpainless.algorithm.HashAlgorithm; -import org.pgpainless.algorithm.PublicKeyAlgorithm; -import org.pgpainless.algorithm.SymmetricKeyAlgorithm; -import org.pgpainless.util.Passphrase; - -public interface CryptoEngineImplementation { - - PBESecretKeyEncryptor getPBESecretKeyEncryptor(PGPSecretKey secretKey, Passphrase passphrase) throws PGPException; - - default PBESecretKeyEncryptor getPBESecretKeyEncryptor(SymmetricKeyAlgorithm symmetricKeyAlgorithm, - Passphrase passphrase) - throws PGPException { - return getPBESecretKeyEncryptor(symmetricKeyAlgorithm, - getPGPDigestCalculator(HashAlgorithm.SHA1), passphrase); - } - - PBESecretKeyEncryptor getPBESecretKeyEncryptor(SymmetricKeyAlgorithm symmetricKeyAlgorithm, - PGPDigestCalculator digestCalculator, - Passphrase passphrase); - - PBESecretKeyDecryptor getPBESecretKeyDecryptor(Passphrase passphrase) throws PGPException; - - PGPDigestCalculatorProvider getPGPDigestCalculatorProvider() throws PGPException; - - default PGPDigestCalculator getPGPDigestCalculator(HashAlgorithm algorithm) throws PGPException { - return getPGPDigestCalculator(algorithm.getAlgorithmId()); - } - - default PGPDigestCalculator getPGPDigestCalculator(int algorithm) throws PGPException { - return getPGPDigestCalculatorProvider().get(algorithm); - } - - PGPContentVerifierBuilderProvider getPGPContentVerifierBuilderProvider(); - - default PGPContentSignerBuilder getPGPContentSignerBuilder(PublicKeyAlgorithm keyAlgorithm, HashAlgorithm hashAlgorithm) { - return getPGPContentSignerBuilder(keyAlgorithm.getAlgorithmId(), hashAlgorithm.getAlgorithmId()); - } - - PGPContentSignerBuilder getPGPContentSignerBuilder(int keyAlgorithm, int hashAlgorithm); - - KeyFingerPrintCalculator getKeyFingerprintCalculator(); - - PBEDataDecryptorFactory getPBEDataDecryptorFactory(Passphrase passphrase) throws PGPException; - - PublicKeyDataDecryptorFactory getPublicKeyDataDecryptorFactory(PGPPrivateKey privateKey); - - PublicKeyKeyEncryptionMethodGenerator getPublicKeyKeyEncryptionMethodGenerator(PGPPublicKey key); - - PBEKeyEncryptionMethodGenerator getPBEKeyEncryptionMethodGenerator(Passphrase passphrase); - - default PGPDataEncryptorBuilder getPGPDataEncryptorBuilder(SymmetricKeyAlgorithm symmetricKeyAlgorithm) { - return getPGPDataEncryptorBuilder(symmetricKeyAlgorithm.getAlgorithmId()); - } - - PGPDataEncryptorBuilder getPGPDataEncryptorBuilder(int symmetricKeyAlgorithm); - - PGPKeyPair getPGPKeyPair(PublicKeyAlgorithm algorithm, KeyPair keyPair, Date creationDate) throws PGPException; - - PGPKeyPair getPGPKeyPair(PublicKeyAlgorithm algorithm, AsymmetricCipherKeyPair keyPair, Date creationDate) throws PGPException, NoSuchAlgorithmException, IOException, InvalidKeySpecException; - - PBESecretKeyEncryptor getPBESecretKeyEncryptor(SymmetricKeyAlgorithm encryptionAlgorithm, - HashAlgorithm hashAlgorithm, - int s2kCount, - Passphrase passphrase) throws PGPException; -} diff --git a/pgpainless-core/src/main/java/org/pgpainless/implementation/ImplementationFactory.java b/pgpainless-core/src/main/java/org/pgpainless/implementation/ImplementationFactory.java index 293f7bee..6a716b6c 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/implementation/ImplementationFactory.java +++ b/pgpainless-core/src/main/java/org/pgpainless/implementation/ImplementationFactory.java @@ -1,15 +1,141 @@ +/* + * Copyright 2020 Paul Schaub. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package org.pgpainless.implementation; -public class ImplementationFactory { +import java.io.IOException; +import java.security.KeyPair; +import java.security.NoSuchAlgorithmException; +import java.security.spec.InvalidKeySpecException; +import java.util.Date; - private static CryptoEngineImplementation FACTORY_IMPLEMENTATION = new BcCryptoEngineImplementation(); +import org.bouncycastle.crypto.AsymmetricCipherKeyPair; +import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.PGPKeyPair; +import org.bouncycastle.openpgp.PGPPrivateKey; +import org.bouncycastle.openpgp.PGPPublicKey; +import org.bouncycastle.openpgp.PGPSecretKey; +import org.bouncycastle.openpgp.operator.KeyFingerPrintCalculator; +import org.bouncycastle.openpgp.operator.PBEDataDecryptorFactory; +import org.bouncycastle.openpgp.operator.PBEKeyEncryptionMethodGenerator; +import org.bouncycastle.openpgp.operator.PBESecretKeyDecryptor; +import org.bouncycastle.openpgp.operator.PBESecretKeyEncryptor; +import org.bouncycastle.openpgp.operator.PGPContentSignerBuilder; +import org.bouncycastle.openpgp.operator.PGPContentVerifierBuilderProvider; +import org.bouncycastle.openpgp.operator.PGPDataEncryptorBuilder; +import org.bouncycastle.openpgp.operator.PGPDigestCalculator; +import org.bouncycastle.openpgp.operator.PGPDigestCalculatorProvider; +import org.bouncycastle.openpgp.operator.PublicKeyDataDecryptorFactory; +import org.bouncycastle.openpgp.operator.PublicKeyKeyEncryptionMethodGenerator; +import org.pgpainless.algorithm.HashAlgorithm; +import org.pgpainless.algorithm.PublicKeyAlgorithm; +import org.pgpainless.algorithm.SymmetricKeyAlgorithm; +import org.pgpainless.util.Passphrase; - public static void setFactoryImplementation(CryptoEngineImplementation implementation) { +public abstract class ImplementationFactory { + + private static ImplementationFactory FACTORY_IMPLEMENTATION = new BcImplementationFactory(); + + public static void setFactoryImplementation(ImplementationFactory implementation) { FACTORY_IMPLEMENTATION = implementation; } - public static CryptoEngineImplementation getInstance() { + public static ImplementationFactory getInstance() { return FACTORY_IMPLEMENTATION; } + public PBESecretKeyEncryptor getPBESecretKeyEncryptor(SymmetricKeyAlgorithm symmetricKeyAlgorithm, + Passphrase passphrase) + throws PGPException { + return getPBESecretKeyEncryptor(symmetricKeyAlgorithm, + getPGPDigestCalculator(HashAlgorithm.SHA1), passphrase); + } + + public PBESecretKeyEncryptor getPBESecretKeyEncryptor(PGPSecretKey secretKey, Passphrase passphrase) throws PGPException { + return FACTORY_IMPLEMENTATION.getPBESecretKeyEncryptor(secretKey, passphrase); + } + + public PBESecretKeyEncryptor getPBESecretKeyEncryptor(SymmetricKeyAlgorithm symmetricKeyAlgorithm, PGPDigestCalculator digestCalculator, Passphrase passphrase) { + return FACTORY_IMPLEMENTATION.getPBESecretKeyEncryptor(symmetricKeyAlgorithm, digestCalculator, passphrase); + } + + public PBESecretKeyDecryptor getPBESecretKeyDecryptor(Passphrase passphrase) throws PGPException { + return FACTORY_IMPLEMENTATION.getPBESecretKeyDecryptor(passphrase); + } + + public PGPDigestCalculator getPGPDigestCalculator(HashAlgorithm algorithm) throws PGPException { + return getPGPDigestCalculator(algorithm.getAlgorithmId()); + } + + public PGPDigestCalculator getPGPDigestCalculator(int algorithm) throws PGPException { + return getPGPDigestCalculatorProvider().get(algorithm); + } + + public PGPDigestCalculatorProvider getPGPDigestCalculatorProvider() throws PGPException { + return FACTORY_IMPLEMENTATION.getPGPDigestCalculatorProvider(); + } + + public PGPContentVerifierBuilderProvider getPGPContentVerifierBuilderProvider() { + return FACTORY_IMPLEMENTATION.getPGPContentVerifierBuilderProvider(); + } + + public PGPContentSignerBuilder getPGPContentSignerBuilder(PublicKeyAlgorithm keyAlgorithm, HashAlgorithm hashAlgorithm) { + return getPGPContentSignerBuilder(keyAlgorithm.getAlgorithmId(), hashAlgorithm.getAlgorithmId()); + } + + public PGPContentSignerBuilder getPGPContentSignerBuilder(int keyAlgorithm, int hashAlgorithm) { + return FACTORY_IMPLEMENTATION.getPGPContentSignerBuilder(keyAlgorithm, hashAlgorithm); + } + + public KeyFingerPrintCalculator getKeyFingerprintCalculator() { + return FACTORY_IMPLEMENTATION.getKeyFingerprintCalculator(); + } + + public PBEDataDecryptorFactory getPBEDataDecryptorFactory(Passphrase passphrase) throws PGPException { + return FACTORY_IMPLEMENTATION.getPBEDataDecryptorFactory(passphrase); + } + + public PublicKeyDataDecryptorFactory getPublicKeyDataDecryptorFactory(PGPPrivateKey privateKey) { + return FACTORY_IMPLEMENTATION.getPublicKeyDataDecryptorFactory(privateKey); + } + + public PublicKeyKeyEncryptionMethodGenerator getPublicKeyKeyEncryptionMethodGenerator(PGPPublicKey key) { + return FACTORY_IMPLEMENTATION.getPublicKeyKeyEncryptionMethodGenerator(key); + } + + public PBEKeyEncryptionMethodGenerator getPBEKeyEncryptionMethodGenerator(Passphrase passphrase) { + return FACTORY_IMPLEMENTATION.getPBEKeyEncryptionMethodGenerator(passphrase); + } + + public PGPDataEncryptorBuilder getPGPDataEncryptorBuilder(SymmetricKeyAlgorithm symmetricKeyAlgorithm) { + return getPGPDataEncryptorBuilder(symmetricKeyAlgorithm.getAlgorithmId()); + } + + public PGPDataEncryptorBuilder getPGPDataEncryptorBuilder(int symmetricKeyAlgorithm) { + return FACTORY_IMPLEMENTATION.getPGPDataEncryptorBuilder(symmetricKeyAlgorithm); + } + + public PGPKeyPair getPGPKeyPair(PublicKeyAlgorithm algorithm, KeyPair keyPair, Date creationDate) throws PGPException { + return FACTORY_IMPLEMENTATION.getPGPKeyPair(algorithm, keyPair, creationDate); + } + + public PGPKeyPair getPGPKeyPair(PublicKeyAlgorithm algorithm, AsymmetricCipherKeyPair keyPair, Date creationDate) throws PGPException, NoSuchAlgorithmException, IOException, InvalidKeySpecException { + return FACTORY_IMPLEMENTATION.getPGPKeyPair(algorithm, keyPair, creationDate); + } + + public PBESecretKeyEncryptor getPBESecretKeyEncryptor(SymmetricKeyAlgorithm encryptionAlgorithm, HashAlgorithm hashAlgorithm, int s2kCount, Passphrase passphrase) throws PGPException { + return FACTORY_IMPLEMENTATION.getPBESecretKeyEncryptor(encryptionAlgorithm, hashAlgorithm, s2kCount, passphrase); + } } diff --git a/pgpainless-core/src/main/java/org/pgpainless/implementation/JceCryptoEngineImplementation.java b/pgpainless-core/src/main/java/org/pgpainless/implementation/JceImplementationFactory.java similarity index 92% rename from pgpainless-core/src/main/java/org/pgpainless/implementation/JceCryptoEngineImplementation.java rename to pgpainless-core/src/main/java/org/pgpainless/implementation/JceImplementationFactory.java index b957d790..59a2ccd4 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/implementation/JceCryptoEngineImplementation.java +++ b/pgpainless-core/src/main/java/org/pgpainless/implementation/JceImplementationFactory.java @@ -1,3 +1,18 @@ +/* + * Copyright 2020 Paul Schaub. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package org.pgpainless.implementation; import java.io.IOException; @@ -47,9 +62,8 @@ import org.pgpainless.algorithm.SymmetricKeyAlgorithm; import org.pgpainless.provider.ProviderFactory; import org.pgpainless.util.Passphrase; -public class JceCryptoEngineImplementation implements CryptoEngineImplementation { +public class JceImplementationFactory extends ImplementationFactory { - @Override public PBESecretKeyEncryptor getPBESecretKeyEncryptor(PGPSecretKey secretKey, Passphrase passphrase) throws PGPException { return new JcePBESecretKeyEncryptorBuilder(secretKey.getKeyEncryptionAlgorithm()) @@ -57,21 +71,18 @@ public class JceCryptoEngineImplementation implements CryptoEngineImplementation .build(passphrase.getChars()); } - @Override public PBESecretKeyEncryptor getPBESecretKeyEncryptor(SymmetricKeyAlgorithm symmetricKeyAlgorithm, PGPDigestCalculator digestCalculator, Passphrase passphrase) { return new JcePBESecretKeyEncryptorBuilder(symmetricKeyAlgorithm.getAlgorithmId(), digestCalculator) .setProvider(ProviderFactory.getProvider()) .build(passphrase.getChars()); } - @Override public PBESecretKeyDecryptor getPBESecretKeyDecryptor(Passphrase passphrase) throws PGPException { return new JcePBESecretKeyDecryptorBuilder(getPGPDigestCalculatorProvider()) .setProvider(ProviderFactory.getProvider()) .build(passphrase.getChars()); } - @Override public PGPDigestCalculatorProvider getPGPDigestCalculatorProvider() throws PGPException { return new JcaPGPDigestCalculatorProviderBuilder() @@ -79,25 +90,21 @@ public class JceCryptoEngineImplementation implements CryptoEngineImplementation .build(); } - @Override public PGPContentVerifierBuilderProvider getPGPContentVerifierBuilderProvider() { return new JcaPGPContentVerifierBuilderProvider() .setProvider(ProviderFactory.getProvider()); } - @Override public PGPContentSignerBuilder getPGPContentSignerBuilder(int keyAlgorithm, int hashAlgorithm) { return new JcaPGPContentSignerBuilder(keyAlgorithm, hashAlgorithm) .setProvider(ProviderFactory.getProvider()); } - @Override public KeyFingerPrintCalculator getKeyFingerprintCalculator() { return new JcaKeyFingerprintCalculator() .setProvider(ProviderFactory.getProvider()); } - @Override public PBEDataDecryptorFactory getPBEDataDecryptorFactory(Passphrase passphrase) throws PGPException { return new JcePBEDataDecryptorFactoryBuilder(getPGPDigestCalculatorProvider()) @@ -105,42 +112,35 @@ public class JceCryptoEngineImplementation implements CryptoEngineImplementation .build(passphrase.getChars()); } - @Override public PublicKeyDataDecryptorFactory getPublicKeyDataDecryptorFactory(PGPPrivateKey privateKey) { return new JcePublicKeyDataDecryptorFactoryBuilder() .setProvider(ProviderFactory.getProvider()) .build(privateKey); } - @Override public PublicKeyKeyEncryptionMethodGenerator getPublicKeyKeyEncryptionMethodGenerator(PGPPublicKey key) { return new JcePublicKeyKeyEncryptionMethodGenerator(key) .setProvider(ProviderFactory.getProvider()); } - @Override public PBEKeyEncryptionMethodGenerator getPBEKeyEncryptionMethodGenerator(Passphrase passphrase) { return new JcePBEKeyEncryptionMethodGenerator(passphrase.getChars()) .setProvider(ProviderFactory.getProvider()); } - @Override public PGPDataEncryptorBuilder getPGPDataEncryptorBuilder(int symmetricKeyAlgorithm) { return new JcePGPDataEncryptorBuilder(symmetricKeyAlgorithm) .setProvider(ProviderFactory.getProvider()); } - @Override public PGPKeyPair getPGPKeyPair(PublicKeyAlgorithm algorithm, KeyPair keyPair, Date creationDate) throws PGPException { return new JcaPGPKeyPair(algorithm.getAlgorithmId(), keyPair, creationDate); } - @Override public PGPKeyPair getPGPKeyPair(PublicKeyAlgorithm algorithm, AsymmetricCipherKeyPair keyPair, Date creationDate) throws PGPException, NoSuchAlgorithmException, IOException, InvalidKeySpecException { return new JcaPGPKeyPair(algorithm.getAlgorithmId(), bcToJceKeyPair(keyPair), creationDate); } - @Override public PBESecretKeyEncryptor getPBESecretKeyEncryptor(SymmetricKeyAlgorithm encryptionAlgorithm, HashAlgorithm hashAlgorithm, int s2kCount, Passphrase passphrase) throws PGPException { return new JcePBESecretKeyEncryptorBuilder( encryptionAlgorithm.getAlgorithmId(),