From 2d6f9738ecc390ae037a70f68a79bb7b3bbce99f Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 9 Nov 2022 22:14:36 +0100 Subject: [PATCH 001/763] PGPainless 1.3.12-SNAPSHOT --- version.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/version.gradle b/version.gradle index 3afdf817..a6e0a54f 100644 --- a/version.gradle +++ b/version.gradle @@ -4,8 +4,8 @@ allprojects { ext { - shortVersion = '1.3.11' - isSnapshot = false + shortVersion = '1.3.12' + isSnapshot = true pgpainlessMinAndroidSdk = 10 javaSourceCompatibility = 1.8 bouncyCastleVersion = '1.72' From 48005da7f3c6ecff2c3a3ad27e7414c9ab023811 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Fri, 11 Nov 2022 13:18:22 +0100 Subject: [PATCH 002/763] SOP : Do not armor already-armored data. --- .../main/java/org/pgpainless/sop/ArmorImpl.java | 16 +++++++++++++++- .../test/java/org/pgpainless/sop/ArmorTest.java | 4 +++- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/ArmorImpl.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/ArmorImpl.java index 71bc3e6b..d65c2925 100644 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/ArmorImpl.java +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/ArmorImpl.java @@ -11,6 +11,7 @@ import java.io.OutputStream; import org.bouncycastle.bcpg.ArmoredOutputStream; import org.bouncycastle.util.io.Streams; +import org.pgpainless.decryption_verification.OpenPgpInputStream; import org.pgpainless.util.ArmoredOutputStreamFactory; import sop.Ready; import sop.enums.ArmorLabel; @@ -31,8 +32,21 @@ public class ArmorImpl implements Armor { public void writeTo(OutputStream outputStream) throws IOException { // By buffering the output stream, we can improve performance drastically BufferedOutputStream bufferedOutputStream = new BufferedOutputStream(outputStream); + + // Determine nature of the given data + OpenPgpInputStream openPgpIn = new OpenPgpInputStream(data); + openPgpIn.reset(); + + if (openPgpIn.isAsciiArmored()) { + // armoring already-armored data is an idempotent operation + Streams.pipeAll(openPgpIn, bufferedOutputStream); + bufferedOutputStream.flush(); + openPgpIn.close(); + return; + } + ArmoredOutputStream armor = ArmoredOutputStreamFactory.get(bufferedOutputStream); - Streams.pipeAll(data, armor); + Streams.pipeAll(openPgpIn, armor); bufferedOutputStream.flush(); armor.close(); } diff --git a/pgpainless-sop/src/test/java/org/pgpainless/sop/ArmorTest.java b/pgpainless-sop/src/test/java/org/pgpainless/sop/ArmorTest.java index 95129dfa..3830912a 100644 --- a/pgpainless-sop/src/test/java/org/pgpainless/sop/ArmorTest.java +++ b/pgpainless-sop/src/test/java/org/pgpainless/sop/ArmorTest.java @@ -29,7 +29,9 @@ public class ArmorTest { @Test public void armor() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, IOException { byte[] data = PGPainless.generateKeyRing().modernKeyRing("Alice").getEncoded(); - byte[] knownGoodArmor = ArmorUtils.toAsciiArmoredString(data).getBytes(StandardCharsets.UTF_8); + byte[] knownGoodArmor = ArmorUtils.toAsciiArmoredString(data) + .replace("Version: PGPainless\n", "") // armor command does not add version anymore + .getBytes(StandardCharsets.UTF_8); byte[] armored = new SOPImpl() .armor() .data(data) From 86b06ee5e3f7040505b5e26f610938820b89eef7 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Fri, 11 Nov 2022 13:45:25 +0100 Subject: [PATCH 003/763] SOP: Hide armor version header by default --- .../util/ArmoredOutputStreamFactory.java | 18 ++++++++++++++---- .../main/java/org/pgpainless/sop/SOPImpl.java | 5 +++++ .../org/pgpainless/sop/ExtractCertTest.java | 2 -- 3 files changed, 19 insertions(+), 6 deletions(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/util/ArmoredOutputStreamFactory.java b/pgpainless-core/src/main/java/org/pgpainless/util/ArmoredOutputStreamFactory.java index 403115ba..67cc1c20 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/util/ArmoredOutputStreamFactory.java +++ b/pgpainless-core/src/main/java/org/pgpainless/util/ArmoredOutputStreamFactory.java @@ -31,7 +31,11 @@ public final class ArmoredOutputStreamFactory { */ public static ArmoredOutputStream get(OutputStream outputStream) { ArmoredOutputStream armoredOutputStream = new ArmoredOutputStream(outputStream); - armoredOutputStream.setHeader(ArmorUtils.HEADER_VERSION, version); + armoredOutputStream.clearHeaders(); + if (version != null && !version.isEmpty()) { + armoredOutputStream.setHeader(ArmorUtils.HEADER_VERSION, version); + } + for (String comment : comment) { ArmorUtils.addCommentHeader(armoredOutputStream, comment); } @@ -55,10 +59,16 @@ public final class ArmoredOutputStreamFactory { * @param versionString version string */ public static void setVersionInfo(String versionString) { - if (versionString == null || versionString.trim().isEmpty()) { - throw new IllegalArgumentException("Version Info MUST NOT be null NOR empty."); + if (versionString == null) { + version = null; + return; + } + String trimmed = versionString.trim(); + if (trimmed.isEmpty()) { + version = null; + } else { + version = trimmed; } - version = versionString; } /** diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/SOPImpl.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/SOPImpl.java index 35ff8994..28772f10 100644 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/SOPImpl.java +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/SOPImpl.java @@ -4,6 +4,7 @@ package org.pgpainless.sop; +import org.pgpainless.util.ArmoredOutputStreamFactory; import sop.SOP; import sop.operation.Armor; import sop.operation.Dearmor; @@ -20,6 +21,10 @@ import sop.operation.Version; public class SOPImpl implements SOP { + static { + ArmoredOutputStreamFactory.setVersionInfo(null); + } + @Override public Version version() { return new VersionImpl(); diff --git a/pgpainless-sop/src/test/java/org/pgpainless/sop/ExtractCertTest.java b/pgpainless-sop/src/test/java/org/pgpainless/sop/ExtractCertTest.java index 84a1f471..b3910482 100644 --- a/pgpainless-sop/src/test/java/org/pgpainless/sop/ExtractCertTest.java +++ b/pgpainless-sop/src/test/java/org/pgpainless/sop/ExtractCertTest.java @@ -18,7 +18,6 @@ import sop.exception.SOPGPException; public class ExtractCertTest { public static final String key = "-----BEGIN PGP PRIVATE KEY BLOCK-----\n" + - "Version: PGPainless\n" + "Comment: A8D9 9FF4 C8DD BBA6 C610 A6B7 9ACB 2195 A9BC DF5B\n" + "Comment: Alice \n" + "\n" + @@ -42,7 +41,6 @@ public class ExtractCertTest { "-----END PGP PRIVATE KEY BLOCK-----\n"; public static final String cert = "-----BEGIN PGP PUBLIC KEY BLOCK-----\n" + - "Version: PGPainless\n" + "Comment: A8D9 9FF4 C8DD BBA6 C610 A6B7 9ACB 2195 A9BC DF5B\n" + "Comment: Alice \n" + "\n" + From 243d64fcb486310d92cfed7bbc88b0e016395b26 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Fri, 11 Nov 2022 13:46:05 +0100 Subject: [PATCH 004/763] Bump sop-java to 4.0.5 and adopt changes (--as=clearsigned) --- .../commands/RoundTripInlineSignInlineVerifyCmdTest.java | 6 +++--- .../src/main/java/org/pgpainless/sop/InlineSignImpl.java | 8 ++++---- .../test/java/org/pgpainless/sop/InlineDetachTest.java | 2 +- .../org/pgpainless/sop/InlineSignVerifyRoundtripTest.java | 2 +- version.gradle | 2 +- 5 files changed, 10 insertions(+), 10 deletions(-) diff --git a/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/RoundTripInlineSignInlineVerifyCmdTest.java b/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/RoundTripInlineSignInlineVerifyCmdTest.java index c80019d1..3d7980b0 100644 --- a/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/RoundTripInlineSignInlineVerifyCmdTest.java +++ b/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/RoundTripInlineSignInlineVerifyCmdTest.java @@ -134,7 +134,7 @@ public class RoundTripInlineSignInlineVerifyCmdTest extends CLITest { pipeStringToStdin(MESSAGE); ByteArrayOutputStream ciphertextOut = pipeStdoutToStream(); assertSuccess(executeCommand("inline-sign", - "--as", "cleartextsigned", + "--as", "clearsigned", key.getAbsolutePath(), "--with-key-password", password.getAbsolutePath())); @@ -154,7 +154,7 @@ public class RoundTripInlineSignInlineVerifyCmdTest extends CLITest { pipeStringToStdin(MESSAGE); ByteArrayOutputStream ciphertextOut = pipeStdoutToStream(); assertSuccess(executeCommand("inline-sign", - "--as", "cleartextsigned", + "--as", "clearsigned", key.getAbsolutePath(), "--with-key-password", password.getAbsolutePath())); @@ -212,7 +212,7 @@ public class RoundTripInlineSignInlineVerifyCmdTest extends CLITest { pipeStringToStdin(MESSAGE); ByteArrayOutputStream ciphertextOut = pipeStdoutToStream(); assertSuccess(executeCommand("inline-sign", - "--as", "cleartextsigned", + "--as", "clearsigned", key.getAbsolutePath(), "--with-key-password", password.getAbsolutePath())); diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/InlineSignImpl.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/InlineSignImpl.java index c0bd29f4..d0cfbfcd 100644 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/InlineSignImpl.java +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/InlineSignImpl.java @@ -32,7 +32,7 @@ import sop.operation.InlineSign; public class InlineSignImpl implements InlineSign { private boolean armor = true; - private InlineSignAs mode = InlineSignAs.Binary; + private InlineSignAs mode = InlineSignAs.binary; private final SigningOptions signingOptions = new SigningOptions(); private final MatchMakingSecretKeyRingProtector protector = new MatchMakingSecretKeyRingProtector(); private final List signingKeys = new ArrayList<>(); @@ -74,7 +74,7 @@ public class InlineSignImpl implements InlineSign { public Ready data(InputStream data) throws SOPGPException.KeyIsProtected, IOException, SOPGPException.ExpectedText { for (PGPSecretKeyRing key : signingKeys) { try { - if (mode == InlineSignAs.CleartextSigned) { + if (mode == InlineSignAs.clearsigned) { signingOptions.addDetachedSignature(protector, key); } else { signingOptions.addInlineSignature(protector, key, modeToSigType(mode)); @@ -87,7 +87,7 @@ public class InlineSignImpl implements InlineSign { } ProducerOptions producerOptions = ProducerOptions.sign(signingOptions); - if (mode == InlineSignAs.CleartextSigned) { + if (mode == InlineSignAs.clearsigned) { producerOptions.setCleartextSigned(); producerOptions.setAsciiArmor(true); } else { @@ -119,7 +119,7 @@ public class InlineSignImpl implements InlineSign { } private static DocumentSignatureType modeToSigType(InlineSignAs mode) { - return mode == InlineSignAs.Binary ? DocumentSignatureType.BINARY_DOCUMENT + return mode == InlineSignAs.binary ? DocumentSignatureType.BINARY_DOCUMENT : DocumentSignatureType.CANONICAL_TEXT_DOCUMENT; } } diff --git a/pgpainless-sop/src/test/java/org/pgpainless/sop/InlineDetachTest.java b/pgpainless-sop/src/test/java/org/pgpainless/sop/InlineDetachTest.java index b4bfe815..1054babb 100644 --- a/pgpainless-sop/src/test/java/org/pgpainless/sop/InlineDetachTest.java +++ b/pgpainless-sop/src/test/java/org/pgpainless/sop/InlineDetachTest.java @@ -63,7 +63,7 @@ public class InlineDetachTest { byte[] cleartextSigned = sop.inlineSign() .key(key) .withKeyPassword("sw0rdf1sh") - .mode(InlineSignAs.CleartextSigned) + .mode(InlineSignAs.clearsigned) .data(data).getBytes(); // actually detach the message diff --git a/pgpainless-sop/src/test/java/org/pgpainless/sop/InlineSignVerifyRoundtripTest.java b/pgpainless-sop/src/test/java/org/pgpainless/sop/InlineSignVerifyRoundtripTest.java index ee892501..b24b729c 100644 --- a/pgpainless-sop/src/test/java/org/pgpainless/sop/InlineSignVerifyRoundtripTest.java +++ b/pgpainless-sop/src/test/java/org/pgpainless/sop/InlineSignVerifyRoundtripTest.java @@ -36,7 +36,7 @@ public class InlineSignVerifyRoundtripTest { byte[] inlineSigned = sop.inlineSign() .key(key) .withKeyPassword("sw0rdf1sh") - .mode(InlineSignAs.CleartextSigned) + .mode(InlineSignAs.clearsigned) .data(message).getBytes(); ByteArrayAndResult> result = sop.inlineVerify() diff --git a/version.gradle b/version.gradle index a6e0a54f..ee8e64fb 100644 --- a/version.gradle +++ b/version.gradle @@ -16,6 +16,6 @@ allprojects { logbackVersion = '1.2.11' mockitoVersion = '4.5.1' slf4jVersion = '1.7.36' - sopJavaVersion = '4.0.3' + sopJavaVersion = '4.0.5' } } From ae88fdf4ab0ba32b819b1dd40d76fb76d815ca44 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Fri, 11 Nov 2022 13:49:28 +0100 Subject: [PATCH 005/763] Document ArmoredOutputStreamFactory.setVersionInfo(null) --- .../java/org/pgpainless/util/ArmoredOutputStreamFactory.java | 1 + 1 file changed, 1 insertion(+) diff --git a/pgpainless-core/src/main/java/org/pgpainless/util/ArmoredOutputStreamFactory.java b/pgpainless-core/src/main/java/org/pgpainless/util/ArmoredOutputStreamFactory.java index 67cc1c20..fb2cd4f5 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/util/ArmoredOutputStreamFactory.java +++ b/pgpainless-core/src/main/java/org/pgpainless/util/ArmoredOutputStreamFactory.java @@ -55,6 +55,7 @@ public final class ArmoredOutputStreamFactory { /** * Overwrite the version header of ASCII armors with a custom value. * Newlines in the version info string result in multiple version header entries. + * If this is set to
null
, then the version header is omitted altogether. * * @param versionString version string */ From 3fc19f56afe48566f0af49fcb034c994390b6198 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Fri, 11 Nov 2022 13:52:53 +0100 Subject: [PATCH 006/763] Update changelog --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index db710e47..b3ff90f0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,12 @@ SPDX-License-Identifier: CC0-1.0 # PGPainless Changelog +## 1.3.12-SNAPSHOT +- Bump `sop-java` to `4.0.5` +- Fix: `sop inline-sign`: Adopt `--as=clearsigned` instead of `--as=cleartextsigned` +- SOP: Hide `Version: PGPainless` armor header in all armored outputs +- Fix: `sop armor`: Do not re-armor already armored data + ## 1.3.11 - Fix: When verifying subkey binding signatures with embedded recycled primary key binding signatures, do not reject signature if primary key binding From 678dac902f4b9bfee8cda0a292e7d11f416b1db7 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Fri, 11 Nov 2022 13:57:28 +0100 Subject: [PATCH 007/763] PGPainless 1.3.12 --- CHANGELOG.md | 2 +- README.md | 2 +- pgpainless-sop/README.md | 4 ++-- version.gradle | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b3ff90f0..f49dcf60 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ SPDX-License-Identifier: CC0-1.0 # PGPainless Changelog -## 1.3.12-SNAPSHOT +## 1.3.12 - Bump `sop-java` to `4.0.5` - Fix: `sop inline-sign`: Adopt `--as=clearsigned` instead of `--as=cleartextsigned` - SOP: Hide `Version: PGPainless` armor header in all armored outputs diff --git a/README.md b/README.md index 09ae92a3..94ea915b 100644 --- a/README.md +++ b/README.md @@ -191,7 +191,7 @@ repositories { } dependencies { - implementation 'org.pgpainless:pgpainless-core:1.3.11' + implementation 'org.pgpainless:pgpainless-core:1.3.12' } ``` diff --git a/pgpainless-sop/README.md b/pgpainless-sop/README.md index 27864e38..ee27b0b5 100644 --- a/pgpainless-sop/README.md +++ b/pgpainless-sop/README.md @@ -23,7 +23,7 @@ To start using pgpainless-sop in your code, include the following lines in your ... dependencies { ... - implementation "org.pgpainless:pgpainless-sop:1.3.11" + implementation "org.pgpainless:pgpainless-sop:1.3.12" ... } @@ -34,7 +34,7 @@ dependencies { org.pgpainless pgpainless-sop - 1.3.11 + 1.3.12 ... diff --git a/version.gradle b/version.gradle index ee8e64fb..b3d2b4f6 100644 --- a/version.gradle +++ b/version.gradle @@ -5,7 +5,7 @@ allprojects { ext { shortVersion = '1.3.12' - isSnapshot = true + isSnapshot = false pgpainlessMinAndroidSdk = 10 javaSourceCompatibility = 1.8 bouncyCastleVersion = '1.72' From 95ba4e46ca2bcfaefbf3c9cb33f32af31accb59e Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Fri, 11 Nov 2022 14:00:36 +0100 Subject: [PATCH 008/763] PGPainless 1.3.13-SNAPSHOT --- version.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/version.gradle b/version.gradle index b3d2b4f6..9bf15d8c 100644 --- a/version.gradle +++ b/version.gradle @@ -4,8 +4,8 @@ allprojects { ext { - shortVersion = '1.3.12' - isSnapshot = false + shortVersion = '1.3.13' + isSnapshot = true pgpainlessMinAndroidSdk = 10 javaSourceCompatibility = 1.8 bouncyCastleVersion = '1.72' From 40715c3e0416d1b36166b25c6cfda4a667efa544 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 23 Nov 2022 20:26:26 +0100 Subject: [PATCH 009/763] Bump sop-java to 4.0.7 --- version.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.gradle b/version.gradle index 9bf15d8c..6d0daa22 100644 --- a/version.gradle +++ b/version.gradle @@ -16,6 +16,6 @@ allprojects { logbackVersion = '1.2.11' mockitoVersion = '4.5.1' slf4jVersion = '1.7.36' - sopJavaVersion = '4.0.5' + sopJavaVersion = '4.0.7' } } From d7c567649db34d103b597522521e3cd8f98f4e3d Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 23 Nov 2022 20:26:43 +0100 Subject: [PATCH 010/763] Improve documentation of bouncyPgVersion --- version.gradle | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/version.gradle b/version.gradle index 6d0daa22..9d05c960 100644 --- a/version.gradle +++ b/version.gradle @@ -9,8 +9,10 @@ allprojects { pgpainlessMinAndroidSdk = 10 javaSourceCompatibility = 1.8 bouncyCastleVersion = '1.72' - // unfortunately we rely on 1.72.1 for a patch for https://github.com/bcgit/bc-java/issues/1257 + // When using bouncyCastleVersion 1.72: + // unfortunately we rely on 1.72.1 or 1.72.3 for a patch for https://github.com/bcgit/bc-java/issues/1257 // which is a bug we introduced with a PR against BC :/ oops + // When bouncyCastleVersion is 1.71, bouncyPgVersion can simply be set to 1.71 as well. bouncyPgVersion = '1.72.1' junitVersion = '5.8.2' logbackVersion = '1.2.11' From 29009cf2246268d76987c58e448f257a7a543a1e Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 23 Nov 2022 20:27:45 +0100 Subject: [PATCH 011/763] Update changelog --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f49dcf60..9b727a6b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,9 @@ SPDX-License-Identifier: CC0-1.0 # PGPainless Changelog +## 1.3.13-SNAPSHOT +- Bump `sop-java` to `4.0.7` + ## 1.3.12 - Bump `sop-java` to `4.0.5` - Fix: `sop inline-sign`: Adopt `--as=clearsigned` instead of `--as=cleartextsigned` From aacfc0f995490eb2a9e467c099aad1e85aa05f12 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 23 Nov 2022 20:33:04 +0100 Subject: [PATCH 012/763] PGPainless 1.3.13 --- CHANGELOG.md | 2 +- README.md | 2 +- pgpainless-sop/README.md | 4 ++-- version.gradle | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9b727a6b..551728aa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ SPDX-License-Identifier: CC0-1.0 # PGPainless Changelog -## 1.3.13-SNAPSHOT +## 1.3.13 - Bump `sop-java` to `4.0.7` ## 1.3.12 diff --git a/README.md b/README.md index 94ea915b..90f6be81 100644 --- a/README.md +++ b/README.md @@ -191,7 +191,7 @@ repositories { } dependencies { - implementation 'org.pgpainless:pgpainless-core:1.3.12' + implementation 'org.pgpainless:pgpainless-core:1.3.13' } ``` diff --git a/pgpainless-sop/README.md b/pgpainless-sop/README.md index ee27b0b5..4a18136d 100644 --- a/pgpainless-sop/README.md +++ b/pgpainless-sop/README.md @@ -23,7 +23,7 @@ To start using pgpainless-sop in your code, include the following lines in your ... dependencies { ... - implementation "org.pgpainless:pgpainless-sop:1.3.12" + implementation "org.pgpainless:pgpainless-sop:1.3.13" ... } @@ -34,7 +34,7 @@ dependencies { org.pgpainless pgpainless-sop - 1.3.12 + 1.3.13 ... diff --git a/version.gradle b/version.gradle index 9d05c960..9cb598cf 100644 --- a/version.gradle +++ b/version.gradle @@ -5,7 +5,7 @@ allprojects { ext { shortVersion = '1.3.13' - isSnapshot = true + isSnapshot = false pgpainlessMinAndroidSdk = 10 javaSourceCompatibility = 1.8 bouncyCastleVersion = '1.72' From ec7237390a5712a2144c1081ffdb571719a80e20 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 23 Nov 2022 20:36:54 +0100 Subject: [PATCH 013/763] PGPainless 1.3.14-SNAPSHOT --- version.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/version.gradle b/version.gradle index 9cb598cf..dc50f086 100644 --- a/version.gradle +++ b/version.gradle @@ -4,8 +4,8 @@ allprojects { ext { - shortVersion = '1.3.13' - isSnapshot = false + shortVersion = '1.3.14' + isSnapshot = true pgpainlessMinAndroidSdk = 10 javaSourceCompatibility = 1.8 bouncyCastleVersion = '1.72' From bc73d26118921f5f07804b9f0248bb28c9d05e94 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 8 Sep 2022 18:23:20 +0200 Subject: [PATCH 014/763] Add Pushdown Automaton for checking OpenPGP message syntax The automaton implements what is described in https://github.com/pgpainless/pgpainless/blob/main/misc/OpenPGPMessageFormat.md However, some differences exist to adopt it to BouncyCastle Part of #237 --- .../PushdownAutomaton.java | 391 ++++++++++++++++++ .../MalformedOpenPgpMessageException.java | 31 ++ .../PushDownAutomatonTest.java | 205 +++++++++ 3 files changed, 627 insertions(+) create mode 100644 pgpainless-core/src/main/java/org/pgpainless/decryption_verification/PushdownAutomaton.java create mode 100644 pgpainless-core/src/main/java/org/pgpainless/exception/MalformedOpenPgpMessageException.java create mode 100644 pgpainless-core/src/test/java/org/pgpainless/decryption_verification/PushDownAutomatonTest.java diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/PushdownAutomaton.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/PushdownAutomaton.java new file mode 100644 index 00000000..6930de4b --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/PushdownAutomaton.java @@ -0,0 +1,391 @@ +package org.pgpainless.decryption_verification; + +import org.bouncycastle.openpgp.PGPCompressedData; +import org.bouncycastle.openpgp.PGPEncryptedDataList; +import org.bouncycastle.openpgp.PGPLiteralData; +import org.bouncycastle.openpgp.PGPOnePassSignatureList; +import org.bouncycastle.openpgp.PGPSignatureList; +import org.pgpainless.exception.MalformedOpenPgpMessageException; + +import java.util.Stack; + +import static org.pgpainless.decryption_verification.PushdownAutomaton.StackAlphabet.msg; +import static org.pgpainless.decryption_verification.PushdownAutomaton.StackAlphabet.ops; +import static org.pgpainless.decryption_verification.PushdownAutomaton.StackAlphabet.terminus; + +/** + * Pushdown Automaton to verify the correct syntax of OpenPGP messages during decryption. + *

+ * OpenPGP messages MUST follow certain rules in order to be well-formed. + * Section §11.3. of RFC4880 specifies a formal grammar for OpenPGP messages. + *

+ * This grammar was transformed into a pushdown automaton, which is implemented below. + * The automaton only ends up in a valid state ({@link #isValid()} iff the OpenPGP message conformed to the + * grammar. + *

+ * There are some specialties with this implementation though: + * Bouncy Castle combines ESKs and Encrypted Data Packets into a single object, so we do not have to + * handle those manually. + *

+ * Bouncy Castle further combines OnePassSignatures and Signatures into lists, so instead of pushing multiple + * 'o's onto the stack repeatedly, a sequence of OnePassSignatures causes a single 'o' to be pushed to the stack. + * The same is true for Signatures. + *

+ * Therefore, a message is valid, even if the number of OnePassSignatures and Signatures does not match. + * If a message contains at least one OnePassSignature, it is sufficient if there is at least one Signature to + * not cause a {@link MalformedOpenPgpMessageException}. + * + * @see RFC4880 §11.3. OpenPGP Messages + */ +public class PushdownAutomaton { + + public enum InputAlphabet { + /** + * A {@link PGPLiteralData} packet. + */ + LiteralData, + /** + * A {@link PGPSignatureList} object. + */ + Signatures, + /** + * A {@link PGPOnePassSignatureList} object. + */ + OnePassSignatures, + /** + * A {@link PGPCompressedData} packet. + * The contents of this packet MUST form a valid OpenPGP message, so a nested PDA is opened to verify + * its nested packet sequence. + */ + CompressedData, + /** + * A {@link PGPEncryptedDataList} object. + * This object combines multiple ESKs and the corresponding Symmetrically Encrypted + * (possibly Integrity Protected) Data packet. + */ + EncryptedData, + /** + * Marks the end of a (sub-) sequence. + * This input is given if the end of an OpenPGP message is reached. + * This might be the case for the end of the whole ciphertext, or the end of a packet with nested contents + * (e.g. the end of a Compressed Data packet). + */ + EndOfSequence + } + + public enum StackAlphabet { + /** + * OpenPGP Message. + */ + msg, + /** + * OnePassSignature (in case of BC this represents a OnePassSignatureList). + */ + ops, + /** + * ESK. Not used, as BC combines encrypted data with their encrypted session keys. + */ + esk, + /** + * Special symbol representing the end of the message. + */ + terminus + } + + /** + * Set of states of the automaton. + * Each state defines its valid transitions in their {@link State#transition(InputAlphabet, PushdownAutomaton)} + * method. + */ + public enum State { + + OpenPgpMessage { + @Override + State transition(InputAlphabet input, PushdownAutomaton automaton) throws MalformedOpenPgpMessageException { + StackAlphabet stackItem = automaton.popStack(); + if (stackItem != msg) { + throw new MalformedOpenPgpMessageException(this, input, stackItem); + } + switch (input) { + + case LiteralData: + return LiteralMessage; + + case Signatures: + automaton.pushStack(msg); + return OpenPgpMessage; + + case OnePassSignatures: + automaton.pushStack(ops); + automaton.pushStack(msg); + return OpenPgpMessage; + + case CompressedData: + return CompressedMessage; + + case EncryptedData: + return EncryptedMessage; + + case EndOfSequence: + default: + throw new MalformedOpenPgpMessageException(this, input, stackItem); + } + } + }, + + LiteralMessage { + @Override + State transition(InputAlphabet input, PushdownAutomaton automaton) throws MalformedOpenPgpMessageException { + StackAlphabet stackItem = automaton.popStack(); + switch (input) { + + case Signatures: + if (stackItem == ops) { + return CorrespondingSignature; + } else { + throw new MalformedOpenPgpMessageException(this, input, stackItem); + } + + case EndOfSequence: + if (stackItem == terminus && automaton.stack.isEmpty()) { + return Valid; + } else { + throw new MalformedOpenPgpMessageException(this, input, stackItem); + } + + case LiteralData: + case OnePassSignatures: + case CompressedData: + case EncryptedData: + default: + throw new MalformedOpenPgpMessageException(this, input, stackItem); + } + } + }, + + CompressedMessage { + @Override + State transition(InputAlphabet input, PushdownAutomaton automaton) throws MalformedOpenPgpMessageException { + StackAlphabet stackItem = automaton.popStack(); + switch (input) { + case Signatures: + if (stackItem == ops) { + return CorrespondingSignature; + } else { + throw new MalformedOpenPgpMessageException(this, input, stackItem); + } + + case EndOfSequence: + if (stackItem == terminus && automaton.stack.isEmpty()) { + return Valid; + } else { + throw new MalformedOpenPgpMessageException(this, input, stackItem); + } + + case LiteralData: + case OnePassSignatures: + case CompressedData: + case EncryptedData: + default: + throw new MalformedOpenPgpMessageException(this, input, stackItem); + } + } + }, + + EncryptedMessage { + @Override + State transition(InputAlphabet input, PushdownAutomaton automaton) throws MalformedOpenPgpMessageException { + StackAlphabet stackItem = automaton.popStack(); + switch (input) { + case Signatures: + if (stackItem == ops) { + return CorrespondingSignature; + } else { + throw new MalformedOpenPgpMessageException(this, input, stackItem); + } + + case EndOfSequence: + if (stackItem == terminus && automaton.stack.isEmpty()) { + return Valid; + } else { + throw new MalformedOpenPgpMessageException(this, input, stackItem); + } + + case LiteralData: + case OnePassSignatures: + case CompressedData: + case EncryptedData: + default: + throw new MalformedOpenPgpMessageException(this, input, stackItem); + } + } + }, + + CorrespondingSignature { + @Override + State transition(InputAlphabet input, PushdownAutomaton automaton) throws MalformedOpenPgpMessageException { + StackAlphabet stackItem = automaton.popStack(); + if (stackItem == terminus && input == InputAlphabet.EndOfSequence && automaton.stack.isEmpty()) { + return Valid; + } else { + throw new MalformedOpenPgpMessageException(this, input, stackItem); + } + } + }, + + Valid { + @Override + State transition(InputAlphabet input, PushdownAutomaton automaton) throws MalformedOpenPgpMessageException { + throw new MalformedOpenPgpMessageException(this, input, null); + } + }, + ; + + /** + * Pop the automatons stack and transition to another state. + * If no valid transition from the current state is available given the popped stack item and input symbol, + * a {@link MalformedOpenPgpMessageException} is thrown. + * Otherwise, the stack is manipulated according to the valid transition and the new state is returned. + * + * @param input input symbol + * @param automaton automaton + * @return new state of the automaton + * @throws MalformedOpenPgpMessageException in case of an illegal input symbol + */ + abstract State transition(InputAlphabet input, PushdownAutomaton automaton) throws MalformedOpenPgpMessageException; + } + + private final Stack stack = new Stack<>(); + private State state; + // Some OpenPGP packets have nested contents (e.g. compressed / encrypted data). + PushdownAutomaton nestedSequence = null; + + public PushdownAutomaton() { + state = State.OpenPgpMessage; + stack.push(terminus); + stack.push(msg); + } + + /** + * Process the next input packet. + * + * @param input input + * @throws MalformedOpenPgpMessageException in case the input packet is illegal here + */ + public void next(InputAlphabet input) throws MalformedOpenPgpMessageException { + _next(input); + } + + /** + * Process the next input packet. + * This method returns true, iff the given input triggered a successful closing of this PDAs nested PDA. + *

+ * This is for example the case, if the current packet is a Compressed Data packet which contains a + * valid nested OpenPGP message and the last input was {@link InputAlphabet#EndOfSequence} indicating the + * end of the Compressed Data packet. + *

+ * If the input triggered this PDAs nested PDA to close its nested PDA, this method returns false + * in order to prevent this PDA from closing its nested PDA prematurely. + * + * @param input input + * @return true if this just closed its nested sequence, false otherwise + * @throws MalformedOpenPgpMessageException if the input is illegal + */ + private boolean _next(InputAlphabet input) throws MalformedOpenPgpMessageException { + if (nestedSequence != null) { + boolean sequenceInNestedSequenceWasClosed = nestedSequence._next(input); + if (sequenceInNestedSequenceWasClosed) return false; // No need to close out nested sequence too. + } else { + // make a state transition in this automaton + state = state.transition(input, this); + + // If the processed packet contains nested sequence, open nested automaton for it + if (input == InputAlphabet.CompressedData || input == InputAlphabet.EncryptedData) { + nestedSequence = new PushdownAutomaton(); + } + } + + if (input != InputAlphabet.EndOfSequence) { + return false; + } + + // Close nested sequence if needed + boolean nestedIsInnerMost = nestedSequence != null && nestedSequence.isInnerMost(); + if (nestedIsInnerMost) { + if (nestedSequence.isValid()) { + // Close nested sequence + nestedSequence = null; + return true; + } else { + throw new MalformedOpenPgpMessageException("Climbing up nested message validation failed." + + " Automaton for current nesting level is not in valid state: " + nestedSequence.getState() + " " + nestedSequence.stack.peek() + " (Input was " + input + ")"); + } + } + return false; + } + + /** + * Return the current state of the PDA. + * + * @return state + */ + private State getState() { + return state; + } + + /** + * Return true, if the PDA is in a valid state (the OpenPGP message is valid). + * + * @return true if valid, false otherwise + */ + public boolean isValid() { + return getState() == State.Valid && stack.isEmpty(); + } + + /** + * Pop an item from the stack. + * + * @return stack item + */ + private StackAlphabet popStack() { + return stack.pop(); + } + + /** + * Push an item onto the stack. + * + * @param item item + */ + private void pushStack(StackAlphabet item) { + stack.push(item); + } + + /** + * Return true, if this packet sequence has no nested sequence. + * A nested sequence is for example the content of a Compressed Data packet. + * + * @return true if PDA is innermost, false if it has a nested sequence + */ + private boolean isInnerMost() { + return nestedSequence == null; + } + + @Override + public String toString() { + StringBuilder out = new StringBuilder("State: ").append(state) + .append(", Stack (asc.): ").append(stack) + .append('\n'); + if (nestedSequence != null) { + // recursively call toString() on nested PDAs and indent their representation + String nestedToString = nestedSequence.toString(); + String[] lines = nestedToString.split("\n"); + for (int i = 0; i < lines.length; i++) { + String nestedLine = lines[i]; + out.append(i == 0 ? "⤷ " : " ") // indent nested PDA + .append(nestedLine) + .append('\n'); + } + } + return out.toString(); + } +} diff --git a/pgpainless-core/src/main/java/org/pgpainless/exception/MalformedOpenPgpMessageException.java b/pgpainless-core/src/main/java/org/pgpainless/exception/MalformedOpenPgpMessageException.java new file mode 100644 index 00000000..07fed365 --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/exception/MalformedOpenPgpMessageException.java @@ -0,0 +1,31 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.exception; + +import org.bouncycastle.openpgp.PGPException; +import org.pgpainless.decryption_verification.PushdownAutomaton; + +/** + * Exception that gets thrown if the OpenPGP message is malformed. + * Malformed messages are messages which do not follow the grammar specified in the RFC. + * + * @see RFC4880 §11.3. OpenPGP Messages + */ +public class MalformedOpenPgpMessageException extends PGPException { + + public MalformedOpenPgpMessageException(String message) { + super(message); + } + + public MalformedOpenPgpMessageException(String message, MalformedOpenPgpMessageException cause) { + super(message, cause); + } + + public MalformedOpenPgpMessageException(PushdownAutomaton.State state, + PushdownAutomaton.InputAlphabet input, + PushdownAutomaton.StackAlphabet stackItem) { + this("Invalid input: There is no legal transition from state '" + state + "' for input '" + input + "' when '" + stackItem + "' is on top of the stack."); + } +} diff --git a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/PushDownAutomatonTest.java b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/PushDownAutomatonTest.java new file mode 100644 index 00000000..1bd07308 --- /dev/null +++ b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/PushDownAutomatonTest.java @@ -0,0 +1,205 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.decryption_verification; + +import org.junit.jupiter.api.Test; +import org.pgpainless.exception.MalformedOpenPgpMessageException; + +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class PushDownAutomatonTest { + + /** + * MSG is valid. + * + * @throws MalformedOpenPgpMessageException fail + */ + @Test + public void testSimpleLiteralMessageIsValid() throws MalformedOpenPgpMessageException { + PushdownAutomaton automaton = new PushdownAutomaton(); + automaton.next(PushdownAutomaton.InputAlphabet.LiteralData); + automaton.next(PushdownAutomaton.InputAlphabet.EndOfSequence); + + assertTrue(automaton.isValid()); + } + + /** + * OPS MSG SIG is valid. + * + * @throws MalformedOpenPgpMessageException fail + */ + @Test + public void testSimpleOpsSignedMesssageIsValid() throws MalformedOpenPgpMessageException { + PushdownAutomaton automaton = new PushdownAutomaton(); + automaton.next(PushdownAutomaton.InputAlphabet.OnePassSignatures); + automaton.next(PushdownAutomaton.InputAlphabet.LiteralData); + automaton.next(PushdownAutomaton.InputAlphabet.Signatures); + automaton.next(PushdownAutomaton.InputAlphabet.EndOfSequence); + + assertTrue(automaton.isValid()); + } + + /** + * SIG MSG is valid. + * + * @throws MalformedOpenPgpMessageException fail + */ + @Test + public void testSimplePrependSignedMessageIsValid() throws MalformedOpenPgpMessageException { + PushdownAutomaton automaton = new PushdownAutomaton(); + automaton.next(PushdownAutomaton.InputAlphabet.Signatures); + automaton.next(PushdownAutomaton.InputAlphabet.LiteralData); + automaton.next(PushdownAutomaton.InputAlphabet.EndOfSequence); + + assertTrue(automaton.isValid()); + } + + /** + * OPS COMP(MSG) SIG is valid. + * + * @throws MalformedOpenPgpMessageException fail + */ + @Test + public void testOPSSignedCompressedMessageIsValid() throws MalformedOpenPgpMessageException { + PushdownAutomaton automaton = new PushdownAutomaton(); + automaton.next(PushdownAutomaton.InputAlphabet.OnePassSignatures); + automaton.next(PushdownAutomaton.InputAlphabet.CompressedData); + automaton.next(PushdownAutomaton.InputAlphabet.LiteralData); + automaton.next(PushdownAutomaton.InputAlphabet.EndOfSequence); + automaton.next(PushdownAutomaton.InputAlphabet.Signatures); + automaton.next(PushdownAutomaton.InputAlphabet.EndOfSequence); + + assertTrue(automaton.isValid()); + } + + /** + * OPS ENC(COMP(COMP(MSG))) SIG is valid. + * + * @throws MalformedOpenPgpMessageException fail + */ + @Test + public void testOpsSignedEncryptedCompressedCompressedMessageIsValid() throws MalformedOpenPgpMessageException { + PushdownAutomaton automaton = new PushdownAutomaton(); + automaton.next(PushdownAutomaton.InputAlphabet.OnePassSignatures); + automaton.next(PushdownAutomaton.InputAlphabet.EncryptedData); + automaton.next(PushdownAutomaton.InputAlphabet.CompressedData); + automaton.next(PushdownAutomaton.InputAlphabet.CompressedData); + + automaton.next(PushdownAutomaton.InputAlphabet.LiteralData); + + automaton.next(PushdownAutomaton.InputAlphabet.EndOfSequence); + automaton.next(PushdownAutomaton.InputAlphabet.EndOfSequence); + automaton.next(PushdownAutomaton.InputAlphabet.EndOfSequence); + automaton.next(PushdownAutomaton.InputAlphabet.Signatures); + automaton.next(PushdownAutomaton.InputAlphabet.EndOfSequence); + + assertTrue(automaton.isValid()); + } + + /** + * MSG SIG is invalid. + * + * @throws MalformedOpenPgpMessageException fail + */ + @Test + public void testLiteralPlusSigsFails() throws MalformedOpenPgpMessageException { + PushdownAutomaton automaton = new PushdownAutomaton(); + automaton.next(PushdownAutomaton.InputAlphabet.LiteralData); + assertThrows(MalformedOpenPgpMessageException.class, + () -> automaton.next(PushdownAutomaton.InputAlphabet.Signatures)); + } + + /** + * MSG MSG is invalid. + * + * @throws MalformedOpenPgpMessageException fail + */ + @Test + public void testTwoLiteralDataPacketsFails() throws MalformedOpenPgpMessageException { + PushdownAutomaton automaton = new PushdownAutomaton(); + automaton.next(PushdownAutomaton.InputAlphabet.LiteralData); + assertThrows(MalformedOpenPgpMessageException.class, + () -> automaton.next(PushdownAutomaton.InputAlphabet.LiteralData)); + } + + /** + * OPS COMP(MSG MSG) SIG is invalid (two literal packets are illegal). + * + * @throws MalformedOpenPgpMessageException fail + */ + @Test + public void testOPSSignedMessageWithTwoLiteralDataPacketsFails() throws MalformedOpenPgpMessageException { + PushdownAutomaton automaton = new PushdownAutomaton(); + automaton.next(PushdownAutomaton.InputAlphabet.OnePassSignatures); + automaton.next(PushdownAutomaton.InputAlphabet.CompressedData); + automaton.next(PushdownAutomaton.InputAlphabet.LiteralData); + assertThrows(MalformedOpenPgpMessageException.class, + () -> automaton.next(PushdownAutomaton.InputAlphabet.LiteralData)); + } + + /** + * OPS COMP(MSG) MSG SIG is invalid. + * + * @throws MalformedOpenPgpMessageException fail + */ + @Test + public void testOPSSignedMessageWithTwoLiteralDataPacketsFails2() throws MalformedOpenPgpMessageException { + PushdownAutomaton automaton = new PushdownAutomaton(); + automaton.next(PushdownAutomaton.InputAlphabet.OnePassSignatures); + automaton.next(PushdownAutomaton.InputAlphabet.CompressedData); + automaton.next(PushdownAutomaton.InputAlphabet.LiteralData); + automaton.next(PushdownAutomaton.InputAlphabet.EndOfSequence); + assertThrows(MalformedOpenPgpMessageException.class, + () -> automaton.next(PushdownAutomaton.InputAlphabet.LiteralData)); + } + + /** + * OPS COMP(MSG SIG) is invalid (MSG SIG does not form valid nested message). + * + * @throws MalformedOpenPgpMessageException fail + */ + @Test + public void testCorrespondingSignaturesOfOpsSignedMessageAreLayerFurtherDownFails() throws MalformedOpenPgpMessageException { + PushdownAutomaton automaton = new PushdownAutomaton(); + automaton.next(PushdownAutomaton.InputAlphabet.OnePassSignatures); + automaton.next(PushdownAutomaton.InputAlphabet.CompressedData); + automaton.next(PushdownAutomaton.InputAlphabet.LiteralData); + assertThrows(MalformedOpenPgpMessageException.class, + () -> automaton.next(PushdownAutomaton.InputAlphabet.Signatures)); + } + + /** + * Empty COMP is invalid. + */ + @Test + public void testEmptyCompressedDataIsInvalid() throws MalformedOpenPgpMessageException { + PushdownAutomaton automaton = new PushdownAutomaton(); + automaton.next(PushdownAutomaton.InputAlphabet.CompressedData); + assertThrows(MalformedOpenPgpMessageException.class, + () -> automaton.next(PushdownAutomaton.InputAlphabet.EndOfSequence)); + } + + @Test + public void testOPSSignedEncryptedCompressedOPSSignedMessageIsValid() throws MalformedOpenPgpMessageException { + PushdownAutomaton automaton = new PushdownAutomaton(); + automaton.next(PushdownAutomaton.InputAlphabet.OnePassSignatures); + + automaton.next(PushdownAutomaton.InputAlphabet.EncryptedData); + automaton.next(PushdownAutomaton.InputAlphabet.OnePassSignatures); + + automaton.next(PushdownAutomaton.InputAlphabet.CompressedData); + automaton.next(PushdownAutomaton.InputAlphabet.LiteralData); + automaton.next(PushdownAutomaton.InputAlphabet.EndOfSequence); + + automaton.next(PushdownAutomaton.InputAlphabet.Signatures); + automaton.next(PushdownAutomaton.InputAlphabet.EndOfSequence); + + automaton.next(PushdownAutomaton.InputAlphabet.Signatures); + automaton.next(PushdownAutomaton.InputAlphabet.EndOfSequence); + + assertTrue(automaton.isValid()); + } +} From bf8949d7f4f4651289baed158de7d61e2d938c59 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 12 Sep 2022 15:28:54 +0200 Subject: [PATCH 015/763] WIP: Implement custom PGPDecryptionStream --- .../pgpainless/algorithm/OpenPgpPacket.java | 71 +++++ .../PGPDecryptionStream.java | 238 ++++++++++++++ .../PushdownAutomaton.java | 6 + .../MalformedOpenPgpMessageException.java | 11 + .../algorithm/OpenPgpPacketTest.java | 38 +++ .../PGPDecryptionStreamTest.java | 290 ++++++++++++++++++ 6 files changed, 654 insertions(+) create mode 100644 pgpainless-core/src/main/java/org/pgpainless/algorithm/OpenPgpPacket.java create mode 100644 pgpainless-core/src/main/java/org/pgpainless/decryption_verification/PGPDecryptionStream.java create mode 100644 pgpainless-core/src/test/java/org/pgpainless/algorithm/OpenPgpPacketTest.java create mode 100644 pgpainless-core/src/test/java/org/pgpainless/decryption_verification/PGPDecryptionStreamTest.java diff --git a/pgpainless-core/src/main/java/org/pgpainless/algorithm/OpenPgpPacket.java b/pgpainless-core/src/main/java/org/pgpainless/algorithm/OpenPgpPacket.java new file mode 100644 index 00000000..63d14a31 --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/algorithm/OpenPgpPacket.java @@ -0,0 +1,71 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.algorithm; + +import org.bouncycastle.bcpg.PacketTags; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.util.HashMap; +import java.util.Map; +import java.util.NoSuchElementException; + +public enum OpenPgpPacket { + PKESK(PacketTags.PUBLIC_KEY_ENC_SESSION), + SIG(PacketTags.SIGNATURE), + SKESK(PacketTags.SYMMETRIC_KEY_ENC_SESSION), + OPS(PacketTags.ONE_PASS_SIGNATURE), + SK(PacketTags.SECRET_KEY), + PK(PacketTags.PUBLIC_KEY), + SSK(PacketTags.SECRET_SUBKEY), + COMP(PacketTags.COMPRESSED_DATA), + SED(PacketTags.SYMMETRIC_KEY_ENC), + MARKER(PacketTags.MARKER), + LIT(PacketTags.LITERAL_DATA), + TRUST(PacketTags.TRUST), + UID(PacketTags.USER_ID), + PSK(PacketTags.PUBLIC_SUBKEY), + UATTR(PacketTags.USER_ATTRIBUTE), + SEIPD(PacketTags.SYM_ENC_INTEGRITY_PRO), + MOD(PacketTags.MOD_DETECTION_CODE), + + EXP_1(PacketTags.EXPERIMENTAL_1), + EXP_2(PacketTags.EXPERIMENTAL_2), + EXP_3(PacketTags.EXPERIMENTAL_3), + EXP_4(PacketTags.EXPERIMENTAL_4), + ; + + static final Map MAP = new HashMap<>(); + + static { + for (OpenPgpPacket p : OpenPgpPacket.values()) { + MAP.put(p.getTag(), p); + } + } + + final int tag; + + @Nullable + public static OpenPgpPacket fromTag(int tag) { + return MAP.get(tag); + } + + @Nonnull + public static OpenPgpPacket requireFromTag(int tag) { + OpenPgpPacket p = fromTag(tag); + if (p == null) { + throw new NoSuchElementException("No OpenPGP packet known for tag " + tag); + } + return p; + } + + OpenPgpPacket(int tag) { + this.tag = tag; + } + + int getTag() { + return tag; + } +} diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/PGPDecryptionStream.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/PGPDecryptionStream.java new file mode 100644 index 00000000..c3053555 --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/PGPDecryptionStream.java @@ -0,0 +1,238 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.decryption_verification; + +import org.bouncycastle.bcpg.BCPGInputStream; +import org.bouncycastle.bcpg.BCPGOutputStream; +import org.bouncycastle.bcpg.ModDetectionCodePacket; +import org.bouncycastle.bcpg.OnePassSignaturePacket; +import org.bouncycastle.bcpg.Packet; +import org.bouncycastle.bcpg.PacketTags; +import org.bouncycastle.bcpg.PublicKeyEncSessionPacket; +import org.bouncycastle.bcpg.SignaturePacket; +import org.bouncycastle.bcpg.SymmetricEncDataPacket; +import org.bouncycastle.bcpg.SymmetricEncIntegrityPacket; +import org.bouncycastle.bcpg.SymmetricKeyEncSessionPacket; +import org.bouncycastle.bcpg.TrustPacket; +import org.bouncycastle.bcpg.UserAttributePacket; +import org.bouncycastle.bcpg.UserIDPacket; +import org.bouncycastle.openpgp.PGPCompressedData; +import org.bouncycastle.openpgp.PGPEncryptedDataList; +import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.PGPLiteralData; +import org.bouncycastle.openpgp.PGPOnePassSignatureList; +import org.bouncycastle.openpgp.PGPSignatureList; +import org.pgpainless.algorithm.OpenPgpPacket; +import org.pgpainless.exception.MalformedOpenPgpMessageException; +import org.pgpainless.implementation.ImplementationFactory; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.NoSuchElementException; +import java.util.Stack; + +public class PGPDecryptionStream extends InputStream { + + PushdownAutomaton automaton = new PushdownAutomaton(); + // nested streams, outermost at the bottom of the stack + Stack packetLayers = new Stack<>(); + + public PGPDecryptionStream(InputStream inputStream) throws IOException, PGPException { + try { + packetLayers.push(Layer.initial(inputStream)); + walkLayer(); + } catch (MalformedOpenPgpMessageException e) { + throw e.toRuntimeException(); + } + } + + private void walkLayer() throws PGPException, IOException { + if (packetLayers.isEmpty()) { + return; + } + + Layer layer = packetLayers.peek(); + BCPGInputStream inputStream = (BCPGInputStream) layer.inputStream; + + loop: while (true) { + if (inputStream.nextPacketTag() == -1) { + popLayer(); + break loop; + } + OpenPgpPacket tag = nextTagOrThrow(inputStream); + switch (tag) { + + case PKESK: + PublicKeyEncSessionPacket pkeskPacket = (PublicKeyEncSessionPacket) inputStream.readPacket(); + PGPEncryptedDataList encList = null; + break; + case SIG: + automaton.next(PushdownAutomaton.InputAlphabet.Signatures); + + ByteArrayOutputStream buf = new ByteArrayOutputStream(); + BCPGOutputStream bcpgOut = new BCPGOutputStream(buf); + while (inputStream.nextPacketTag() == PacketTags.SIGNATURE || inputStream.nextPacketTag() == PacketTags.MARKER) { + Packet packet = inputStream.readPacket(); + if (packet instanceof SignaturePacket) { + SignaturePacket sig = (SignaturePacket) packet; + sig.encode(bcpgOut); + } + } + PGPSignatureList signatures = (PGPSignatureList) ImplementationFactory.getInstance() + .getPGPObjectFactory(buf.toByteArray()).nextObject(); + break; + case SKESK: + SymmetricKeyEncSessionPacket skeskPacket = (SymmetricKeyEncSessionPacket) inputStream.readPacket(); + + break; + case OPS: + automaton.next(PushdownAutomaton.InputAlphabet.OnePassSignatures); + buf = new ByteArrayOutputStream(); + bcpgOut = new BCPGOutputStream(buf); + while (inputStream.nextPacketTag() == PacketTags.ONE_PASS_SIGNATURE || inputStream.nextPacketTag() == PacketTags.MARKER) { + Packet packet = inputStream.readPacket(); + if (packet instanceof OnePassSignaturePacket) { + OnePassSignaturePacket sig = (OnePassSignaturePacket) packet; + sig.encode(bcpgOut); + } + } + PGPOnePassSignatureList onePassSignatures = (PGPOnePassSignatureList) ImplementationFactory.getInstance() + .getPGPObjectFactory(buf.toByteArray()).nextObject(); + break; + case SK: + break; + case PK: + break; + case SSK: + break; + case COMP: + automaton.next(PushdownAutomaton.InputAlphabet.CompressedData); + PGPCompressedData compressedData = new PGPCompressedData(inputStream); + inputStream = new BCPGInputStream(compressedData.getDataStream()); + packetLayers.push(Layer.CompressedData(inputStream)); + break; + case SED: + automaton.next(PushdownAutomaton.InputAlphabet.EncryptedData); + SymmetricEncDataPacket symmetricEncDataPacket = (SymmetricEncDataPacket) inputStream.readPacket(); + break; + case MARKER: + inputStream.readPacket(); // discard + break; + case LIT: + automaton.next(PushdownAutomaton.InputAlphabet.LiteralData); + PGPLiteralData literalData = new PGPLiteralData(inputStream); + packetLayers.push(Layer.LiteralMessage(literalData.getDataStream())); + break loop; + case TRUST: + TrustPacket trustPacket = (TrustPacket) inputStream.readPacket(); + break; + case UID: + UserIDPacket userIDPacket = (UserIDPacket) inputStream.readPacket(); + break; + case PSK: + break; + case UATTR: + UserAttributePacket userAttributePacket = (UserAttributePacket) inputStream.readPacket(); + break; + case SEIPD: + automaton.next(PushdownAutomaton.InputAlphabet.EncryptedData); + SymmetricEncIntegrityPacket symmetricEncIntegrityPacket = (SymmetricEncIntegrityPacket) inputStream.readPacket(); + break; + case MOD: + ModDetectionCodePacket modDetectionCodePacket = (ModDetectionCodePacket) inputStream.readPacket(); + break; + case EXP_1: + break; + case EXP_2: + break; + case EXP_3: + break; + case EXP_4: + break; + } + } + } + + private OpenPgpPacket nextTagOrThrow(BCPGInputStream inputStream) + throws IOException, InvalidOpenPgpPacketException { + try { + return OpenPgpPacket.requireFromTag(inputStream.nextPacketTag()); + } catch (NoSuchElementException e) { + throw new InvalidOpenPgpPacketException(e.getMessage()); + } + } + + private void popLayer() throws MalformedOpenPgpMessageException { + if (packetLayers.pop().isNested) + automaton.next(PushdownAutomaton.InputAlphabet.EndOfSequence); + } + + @Override + public int read() throws IOException { + if (packetLayers.isEmpty()) { + try { + automaton.assertValid(); + } catch (MalformedOpenPgpMessageException e) { + throw e.toRuntimeException(); + } + return -1; + } + + int r = -1; + try { + r = packetLayers.peek().inputStream.read(); + } catch (IOException e) { + } + if (r == -1) { + try { + popLayer(); + walkLayer(); + } catch (MalformedOpenPgpMessageException e) { + throw e.toRuntimeException(); + } + catch (PGPException e) { + throw new RuntimeException(e); + } + return read(); + } + return r; + } + + public static class InvalidOpenPgpPacketException extends PGPException { + + public InvalidOpenPgpPacketException(String message) { + super(message); + } + } + + private static class Layer { + InputStream inputStream; + boolean isNested; + + private Layer(InputStream inputStream, boolean isNested) { + this.inputStream = inputStream; + this.isNested = isNested; + } + + static Layer initial(InputStream inputStream) { + BCPGInputStream bcpgIn; + if (inputStream instanceof BCPGInputStream) { + bcpgIn = (BCPGInputStream) inputStream; + } else { + bcpgIn = new BCPGInputStream(inputStream); + } + return new Layer(bcpgIn, true); + } + + static Layer LiteralMessage(InputStream inputStream) { + return new Layer(inputStream, false); + } + + static Layer CompressedData(InputStream inputStream) { + return new Layer(inputStream, true); + } + } +} diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/PushdownAutomaton.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/PushdownAutomaton.java index 6930de4b..86075280 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/PushdownAutomaton.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/PushdownAutomaton.java @@ -342,6 +342,12 @@ public class PushdownAutomaton { return getState() == State.Valid && stack.isEmpty(); } + public void assertValid() throws MalformedOpenPgpMessageException { + if (!isValid()) { + throw new MalformedOpenPgpMessageException("Pushdown Automaton is not in an acceptable state: " + toString()); + } + } + /** * Pop an item from the stack. * diff --git a/pgpainless-core/src/main/java/org/pgpainless/exception/MalformedOpenPgpMessageException.java b/pgpainless-core/src/main/java/org/pgpainless/exception/MalformedOpenPgpMessageException.java index 07fed365..9ce2284d 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/exception/MalformedOpenPgpMessageException.java +++ b/pgpainless-core/src/main/java/org/pgpainless/exception/MalformedOpenPgpMessageException.java @@ -28,4 +28,15 @@ public class MalformedOpenPgpMessageException extends PGPException { PushdownAutomaton.StackAlphabet stackItem) { this("Invalid input: There is no legal transition from state '" + state + "' for input '" + input + "' when '" + stackItem + "' is on top of the stack."); } + + public RTE toRuntimeException() { + return new RTE(this); + } + + public static class RTE extends RuntimeException { + + public RTE(MalformedOpenPgpMessageException e) { + super(e); + } + } } diff --git a/pgpainless-core/src/test/java/org/pgpainless/algorithm/OpenPgpPacketTest.java b/pgpainless-core/src/test/java/org/pgpainless/algorithm/OpenPgpPacketTest.java new file mode 100644 index 00000000..88146605 --- /dev/null +++ b/pgpainless-core/src/test/java/org/pgpainless/algorithm/OpenPgpPacketTest.java @@ -0,0 +1,38 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.algorithm; + +import org.bouncycastle.bcpg.PacketTags; +import org.junit.jupiter.api.Test; + +import java.util.NoSuchElementException; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; + +public class OpenPgpPacketTest { + + @Test + public void testFromInvalidTag() { + int tag = PacketTags.RESERVED; + assertNull(OpenPgpPacket.fromTag(tag)); + assertThrows(NoSuchElementException.class, + () -> OpenPgpPacket.requireFromTag(tag)); + } + + @Test + public void testFromExistingTags() { + for (OpenPgpPacket p : OpenPgpPacket.values()) { + assertEquals(p, OpenPgpPacket.fromTag(p.getTag())); + assertEquals(p, OpenPgpPacket.requireFromTag(p.getTag())); + } + } + + @Test + public void testPKESKTagMatches() { + assertEquals(PacketTags.PUBLIC_KEY_ENC_SESSION, OpenPgpPacket.PKESK.getTag()); + } +} diff --git a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/PGPDecryptionStreamTest.java b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/PGPDecryptionStreamTest.java new file mode 100644 index 00000000..bb2742b5 --- /dev/null +++ b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/PGPDecryptionStreamTest.java @@ -0,0 +1,290 @@ +package org.pgpainless.decryption_verification; + +import org.bouncycastle.bcpg.ArmoredInputStream; +import org.bouncycastle.bcpg.ArmoredOutputStream; +import org.bouncycastle.bcpg.CompressionAlgorithmTags; +import org.bouncycastle.openpgp.PGPCompressedDataGenerator; +import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.PGPLiteralData; +import org.bouncycastle.openpgp.PGPLiteralDataGenerator; +import org.bouncycastle.openpgp.PGPSecretKeyRing; +import org.bouncycastle.openpgp.PGPSignature; +import org.bouncycastle.util.io.Streams; +import org.junit.jupiter.api.Test; +import org.pgpainless.PGPainless; +import org.pgpainless.encryption_signing.EncryptionResult; +import org.pgpainless.encryption_signing.EncryptionStream; +import org.pgpainless.encryption_signing.ProducerOptions; +import org.pgpainless.encryption_signing.SigningOptions; +import org.pgpainless.exception.MalformedOpenPgpMessageException; +import org.pgpainless.key.protection.SecretKeyRingProtector; +import org.pgpainless.util.ArmoredInputStreamFactory; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.nio.charset.StandardCharsets; +import java.security.InvalidAlgorithmParameterException; +import java.security.NoSuchAlgorithmException; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +public class PGPDecryptionStreamTest { + + private static final String KEY = "-----BEGIN PGP PRIVATE KEY BLOCK-----\n" + + "Version: PGPainless\n" + + "Comment: DA05 848F 37D4 68E6 F982 C889 7A70 1FC6 904D 3F4C\n" + + "Comment: Alice \n" + + "\n" + + "lFgEYxzSCBYJKwYBBAHaRw8BAQdAeJU8m4GOJb1eQgv/ryilFHRfNLTYFMNqL6zj\n" + + "r0vF7dsAAP42rAtngpJ6dZxoZlJX0Je65zk1VMPeTrXaWfPS2HSKBRGptBxBbGlj\n" + + "ZSA8YWxpY2VAcGdwYWlubGVzcy5vcmc+iI8EExYKAEEFAmMc0ggJEHpwH8aQTT9M\n" + + "FiEE2gWEjzfUaOb5gsiJenAfxpBNP0wCngECmwEFFgIDAQAECwkIBwUVCgkICwKZ\n" + + "AQAApZEBALUXHtvswPZG28YO+16Men6/fpk+scvqpNMnD4ty3IkAAPwK6TuXjNnZ\n" + + "0XuWdnilvLMV23Ai1d5g6em+lwLK5M2SApxdBGMc0ggSCisGAQQBl1UBBQEBB0D8\n" + + "mNUVX8y2MXFaSeFYqOTPFnGT7dgNVdn6yc0UtkkHOgMBCAcAAP9y9OtP4SX9voPb\n" + + "ID2u9PkJKgo4hTB8NK5LouGppdRtEBGriHUEGBYKAB0FAmMc0ggCngECmwwFFgID\n" + + "AQAECwkIBwUVCgkICwAKCRB6cB/GkE0/TAywAQDpZRJS/joFH4+xcwheqWfI7ay/\n" + + "WfojUoGQMYGnUjsgYwEAkceRUsgkqI0SVgYvuglfaQpZ9k2ns1mZGVLkXvu/yQyc\n" + + "WARjHNIIFgkrBgEEAdpHDwEBB0BGN9BybSOrj8B6gim1SjbB/IiqAshlzMDunVkQ\n" + + "X23npQABAJqvjOOY7qhBuTusC5/Q5+25iLrhMn4TI+LXlJHMVNOaE0OI1QQYFgoA\n" + + "fQUCYxzSCAKeAQKbAgUWAgMBAAQLCQgHBRUKCQgLXyAEGRYKAAYFAmMc0ggACgkQ\n" + + "KALh4BJQXl6yTQD/dh0N5228Uwtu7XHy6dmpMRX62cac5tXQ9WaDzpy8STgBAMdn\n" + + "Mq948UOYEhdk/ZY2/hwux/4t+FHvqrXW8ziBe4cLAAoJEHpwH8aQTT9M1hQA/3Ms\n" + + "P3kzoed3VsWu1ZMr7dKEngbc6SoJ2XPayzN0QYJaAQCIY5NcT9mZF97HWV3Vgeum\n" + + "00sWMHXfkW3+nl5OpUZaDA==\n" + + "=THgv\n" + + "-----END PGP PRIVATE KEY BLOCK-----"; + + private static final String PLAINTEXT = "Hello, World!\n"; + + private static final String LIT = "" + + "-----BEGIN PGP MESSAGE-----\n" + + "Version: PGPainless\n" + + "\n" + + "yxRiAAAAAABIZWxsbywgV29ybGQhCg==\n" + + "=WGju\n" + + "-----END PGP MESSAGE-----"; + + private static final String LIT_LIT = "" + + "-----BEGIN PGP MESSAGE-----\n" + + "Version: BCPG v1.71\n" + + "\n" + + "yxRiAAAAAABIZWxsbywgV29ybGQhCssUYgAAAAAASGVsbG8sIFdvcmxkIQo=\n" + + "=A91Q\n" + + "-----END PGP MESSAGE-----"; + + private static final String COMP_LIT = "" + + "-----BEGIN PGP MESSAGE-----\n" + + "Version: BCPG v1.71\n" + + "\n" + + "owE7LZLEAAIeqTk5+ToK4flFOSmKXAA=\n" + + "=ZYDg\n" + + "-----END PGP MESSAGE-----"; + + private static final String COMP = "" + + "-----BEGIN PGP MESSAGE-----\n" + + "Version: BCPG v1.71\n" + + "\n" + + "owEDAA==\n" + + "=MDzg\n" + + "-----END PGP MESSAGE-----"; + + private static final String COMP_COMP_LIT = "" + + "-----BEGIN PGP MESSAGE-----\n" + + "Version: BCPG v1.71\n" + + "\n" + + "owEBRwC4/6MDQlpoOTFBWSZTWVuW2KAAAAr3hGAQBABgBABAAIAWBJAAAAggADFM\n" + + "ABNBqBo00N6puqWR+TqInoXQ58XckU4UJBbltigA\n" + + "=K9Zl\n" + + "-----END PGP MESSAGE-----"; + + private static final String SIG_LIT = "" + + "-----BEGIN PGP MESSAGE-----\n" + + "Version: BCPG v1.71\n" + + "\n" + + "iHUEABYKACcFAmMc1i0JECgC4eASUF5eFiEEjN3RiJxCf/TyYOQjKALh4BJQXl4A\n" + + "AHkrAP98uPpqrgIix7epgL7MM1cjXXGSxqbDfXHwgptk1YGQlgD/fw89VGcXwFaI\n" + + "2k7kpXQYy/1BqnovM/jZ3X3mXhhTaAOjATstksQAAh6pOTn5Ogrh+UU5KYpcAA==\n" + + "=WKPn\n" + + "-----END PGP MESSAGE-----"; + + @Test + public void genLIT() throws IOException { + ArmoredOutputStream armorOut = new ArmoredOutputStream(System.out); + PGPLiteralDataGenerator litGen = new PGPLiteralDataGenerator(); + OutputStream litOut = litGen.open(armorOut, PGPLiteralDataGenerator.BINARY, "", PGPLiteralData.NOW, new byte[1 << 9]); + litOut.write(PLAINTEXT.getBytes(StandardCharsets.UTF_8)); + litOut.close(); + armorOut.close(); + } + + @Test + public void processLIT() throws IOException, PGPException { + ByteArrayInputStream bytesIn = new ByteArrayInputStream(LIT.getBytes(StandardCharsets.UTF_8)); + ArmoredInputStream armorIn = ArmoredInputStreamFactory.get(bytesIn); + PGPDecryptionStream decIn = new PGPDecryptionStream(armorIn); + + ByteArrayOutputStream out = new ByteArrayOutputStream(); + Streams.pipeAll(decIn, out); + assertEquals(PLAINTEXT, out.toString()); + armorIn.close(); + } + + @Test + public void getLIT_LIT() throws IOException { + ArmoredOutputStream armorOut = new ArmoredOutputStream(System.out); + PGPLiteralDataGenerator litGen = new PGPLiteralDataGenerator(); + OutputStream litOut = litGen.open(armorOut, PGPLiteralDataGenerator.BINARY, "", PGPLiteralData.NOW, new byte[1 << 9]); + litOut.write(PLAINTEXT.getBytes(StandardCharsets.UTF_8)); + litOut.close(); + + litOut = litGen.open(armorOut, PGPLiteralDataGenerator.BINARY, "", PGPLiteralData.NOW, new byte[1 << 9]); + litOut.write(PLAINTEXT.getBytes(StandardCharsets.UTF_8)); + litOut.close(); + + armorOut.close(); + } + + @Test + public void processLIT_LIT() throws IOException, PGPException { + ByteArrayInputStream bytesIn = new ByteArrayInputStream(LIT_LIT.getBytes(StandardCharsets.UTF_8)); + ArmoredInputStream armorIn = ArmoredInputStreamFactory.get(bytesIn); + PGPDecryptionStream decIn = new PGPDecryptionStream(armorIn); + + ByteArrayOutputStream out = new ByteArrayOutputStream(); + assertThrows(MalformedOpenPgpMessageException.RTE.class, () -> Streams.pipeAll(decIn, out)); + } + + @Test + public void genCOMP_LIT() throws IOException { + ArmoredOutputStream armorOut = new ArmoredOutputStream(System.out); + PGPCompressedDataGenerator compGen = new PGPCompressedDataGenerator(CompressionAlgorithmTags.ZIP); + OutputStream compOut = compGen.open(armorOut); + PGPLiteralDataGenerator litGen = new PGPLiteralDataGenerator(); + OutputStream litOut = litGen.open(compOut, PGPLiteralDataGenerator.BINARY, "", PGPLiteralData.NOW, new byte[1 << 9]); + litOut.write(PLAINTEXT.getBytes(StandardCharsets.UTF_8)); + litOut.close(); + compOut.close(); + armorOut.close(); + } + + @Test + public void processCOMP_LIT() throws IOException, PGPException { + ByteArrayInputStream bytesIn = new ByteArrayInputStream(COMP_LIT.getBytes(StandardCharsets.UTF_8)); + ArmoredInputStream armorIn = ArmoredInputStreamFactory.get(bytesIn); + PGPDecryptionStream decIn = new PGPDecryptionStream(armorIn); + + ByteArrayOutputStream out = new ByteArrayOutputStream(); + Streams.pipeAll(decIn, out); + decIn.close(); + armorIn.close(); + + assertEquals(PLAINTEXT, out.toString()); + } + + @Test + public void genCOMP() throws IOException { + ArmoredOutputStream armorOut = new ArmoredOutputStream(System.out); + PGPCompressedDataGenerator compGen = new PGPCompressedDataGenerator(CompressionAlgorithmTags.ZIP); + OutputStream compOut = compGen.open(armorOut); + compOut.close(); + armorOut.close(); + } + + @Test + public void processCOMP() throws IOException { + ByteArrayInputStream bytesIn = new ByteArrayInputStream(COMP.getBytes(StandardCharsets.UTF_8)); + ArmoredInputStream armorIn = ArmoredInputStreamFactory.get(bytesIn); + assertThrows(MalformedOpenPgpMessageException.RTE.class, () -> { + PGPDecryptionStream decIn = new PGPDecryptionStream(armorIn); + Streams.drain(decIn); + }); + } + + @Test + public void genCOMP_COMP_LIT() throws IOException { + ArmoredOutputStream armorOut = new ArmoredOutputStream(System.out); + + PGPCompressedDataGenerator compGen1 = new PGPCompressedDataGenerator(CompressionAlgorithmTags.ZIP); + OutputStream compOut1 = compGen1.open(armorOut); + + PGPCompressedDataGenerator compGen2 = new PGPCompressedDataGenerator(CompressionAlgorithmTags.BZIP2); + OutputStream compOut2 = compGen2.open(compOut1); + + PGPLiteralDataGenerator litGen = new PGPLiteralDataGenerator(); + OutputStream litOut = litGen.open(compOut2, PGPLiteralDataGenerator.BINARY, "", PGPLiteralDataGenerator.NOW, new byte[1 << 9]); + + litOut.write(PLAINTEXT.getBytes(StandardCharsets.UTF_8)); + litOut.close(); + compOut2.close(); + compOut1.close(); + armorOut.close(); + } + + @Test + public void processCOMP_COMP_LIT() throws PGPException, IOException { + ByteArrayInputStream bytesIn = new ByteArrayInputStream(COMP_COMP_LIT.getBytes(StandardCharsets.UTF_8)); + ArmoredInputStream armorIn = ArmoredInputStreamFactory.get(bytesIn); + PGPDecryptionStream decIn = new PGPDecryptionStream(armorIn); + + ByteArrayOutputStream out = new ByteArrayOutputStream(); + Streams.pipeAll(decIn, out); + decIn.close(); + + assertEquals(PLAINTEXT, out.toString()); + } + + @Test + public void genKey() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, IOException { + System.out.println(PGPainless.asciiArmor( + PGPainless.generateKeyRing().modernKeyRing("Alice ") + )); + } + + @Test + public void genSIG_LIT() throws PGPException, IOException { + PGPSecretKeyRing secretKeys = PGPainless.readKeyRing().secretKeyRing(KEY); + ByteArrayOutputStream msgOut = new ByteArrayOutputStream(); + EncryptionStream signer = PGPainless.encryptAndOrSign() + .onOutputStream(msgOut) + .withOptions( + ProducerOptions.sign( + SigningOptions.get() + .addDetachedSignature(SecretKeyRingProtector.unprotectedKeys(), secretKeys) + ).setAsciiArmor(false) + ); + + Streams.pipeAll(new ByteArrayInputStream(PLAINTEXT.getBytes(StandardCharsets.UTF_8)), signer); + signer.close(); + EncryptionResult result = signer.getResult(); + PGPSignature detachedSignature = result.getDetachedSignatures().get(result.getDetachedSignatures().keySet().iterator().next()).iterator().next(); + + ByteArrayOutputStream out = new ByteArrayOutputStream(); + ArmoredOutputStream armorOut = new ArmoredOutputStream(out); + armorOut.flush(); + detachedSignature.encode(armorOut); + armorOut.write(msgOut.toByteArray()); + armorOut.close(); + + String armored = out.toString(); + System.out.println(armored + .replace("-----BEGIN PGP SIGNATURE-----\n", "-----BEGIN PGP MESSAGE-----\n") + .replace("-----END PGP SIGNATURE-----", "-----END PGP MESSAGE-----")); + } + + @Test + public void processSIG_LIT() throws IOException, PGPException { + ByteArrayInputStream bytesIn = new ByteArrayInputStream(SIG_LIT.getBytes(StandardCharsets.UTF_8)); + ArmoredInputStream armorIn = ArmoredInputStreamFactory.get(bytesIn); + PGPDecryptionStream decIn = new PGPDecryptionStream(armorIn); + + ByteArrayOutputStream out = new ByteArrayOutputStream(); + Streams.pipeAll(decIn, out); + decIn.close(); + + System.out.println(out); + } +} From e86062c427dde86b3d75c5af2b3aa1f75c6b11a1 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 13 Sep 2022 19:23:59 +0200 Subject: [PATCH 016/763] WIP: Replace nesting with independent instancing --- ...ream.java => MessageDecryptionStream.java} | 210 ++++++----- .../OpenPgpMessageInputStream.java | 331 ++++++++++++++++++ .../automaton/InputAlphabet.java | 41 +++ .../NestingPDA.java} | 90 +---- .../automaton/PDA.java | 237 +++++++++++++ .../automaton/StackAlphabet.java | 20 ++ .../MalformedOpenPgpMessageException.java | 25 +- .../org/pgpainless/key/info/KeyRingInfo.java | 40 ++- .../OpenPgpMessageInputStreamTest.java | 86 +++++ .../PGPDecryptionStreamTest.java | 73 +++- .../PushDownAutomatonTest.java | 205 ----------- .../automaton/NestingPDATest.java | 205 +++++++++++ .../automaton/PDATest.java | 75 ++++ 13 files changed, 1227 insertions(+), 411 deletions(-) rename pgpainless-core/src/main/java/org/pgpainless/decryption_verification/{PGPDecryptionStream.java => MessageDecryptionStream.java} (59%) create mode 100644 pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java create mode 100644 pgpainless-core/src/main/java/org/pgpainless/decryption_verification/automaton/InputAlphabet.java rename pgpainless-core/src/main/java/org/pgpainless/decryption_verification/{PushdownAutomaton.java => automaton/NestingPDA.java} (78%) create mode 100644 pgpainless-core/src/main/java/org/pgpainless/decryption_verification/automaton/PDA.java create mode 100644 pgpainless-core/src/main/java/org/pgpainless/decryption_verification/automaton/StackAlphabet.java create mode 100644 pgpainless-core/src/test/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStreamTest.java delete mode 100644 pgpainless-core/src/test/java/org/pgpainless/decryption_verification/PushDownAutomatonTest.java create mode 100644 pgpainless-core/src/test/java/org/pgpainless/decryption_verification/automaton/NestingPDATest.java create mode 100644 pgpainless-core/src/test/java/org/pgpainless/decryption_verification/automaton/PDATest.java diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/PGPDecryptionStream.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/MessageDecryptionStream.java similarity index 59% rename from pgpainless-core/src/main/java/org/pgpainless/decryption_verification/PGPDecryptionStream.java rename to pgpainless-core/src/main/java/org/pgpainless/decryption_verification/MessageDecryptionStream.java index c3053555..335e9d57 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/PGPDecryptionStream.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/MessageDecryptionStream.java @@ -12,41 +12,50 @@ import org.bouncycastle.bcpg.Packet; import org.bouncycastle.bcpg.PacketTags; import org.bouncycastle.bcpg.PublicKeyEncSessionPacket; import org.bouncycastle.bcpg.SignaturePacket; -import org.bouncycastle.bcpg.SymmetricEncDataPacket; -import org.bouncycastle.bcpg.SymmetricEncIntegrityPacket; import org.bouncycastle.bcpg.SymmetricKeyEncSessionPacket; -import org.bouncycastle.bcpg.TrustPacket; -import org.bouncycastle.bcpg.UserAttributePacket; -import org.bouncycastle.bcpg.UserIDPacket; 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.PGPOnePassSignatureList; +import org.bouncycastle.openpgp.PGPPBEEncryptedData; import org.bouncycastle.openpgp.PGPSignatureList; +import org.bouncycastle.openpgp.operator.PBEDataDecryptorFactory; import org.pgpainless.algorithm.OpenPgpPacket; +import org.pgpainless.decryption_verification.automaton.InputAlphabet; +import org.pgpainless.decryption_verification.automaton.NestingPDA; import org.pgpainless.exception.MalformedOpenPgpMessageException; +import org.pgpainless.exception.MessageNotIntegrityProtectedException; +import org.pgpainless.exception.MissingDecryptionMethodException; import org.pgpainless.implementation.ImplementationFactory; +import org.pgpainless.util.Passphrase; +import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; +import java.io.SequenceInputStream; +import java.util.ArrayList; +import java.util.List; import java.util.NoSuchElementException; import java.util.Stack; -public class PGPDecryptionStream extends InputStream { +public class MessageDecryptionStream extends InputStream { - PushdownAutomaton automaton = new PushdownAutomaton(); + private final ConsumerOptions options; + + NestingPDA automaton = new NestingPDA(); // nested streams, outermost at the bottom of the stack Stack packetLayers = new Stack<>(); + List pkeskList = new ArrayList<>(); + List skeskList = new ArrayList<>(); - public PGPDecryptionStream(InputStream inputStream) throws IOException, PGPException { - try { - packetLayers.push(Layer.initial(inputStream)); - walkLayer(); - } catch (MalformedOpenPgpMessageException e) { - throw e.toRuntimeException(); - } + public MessageDecryptionStream(InputStream inputStream, ConsumerOptions options) + throws IOException, PGPException { + this.options = options; + packetLayers.push(Layer.initial(inputStream)); + walkLayer(); } private void walkLayer() throws PGPException, IOException { @@ -54,6 +63,7 @@ public class PGPDecryptionStream extends InputStream { return; } + // We are currently in the deepest layer Layer layer = packetLayers.peek(); BCPGInputStream inputStream = (BCPGInputStream) layer.inputStream; @@ -65,33 +75,23 @@ public class PGPDecryptionStream extends InputStream { OpenPgpPacket tag = nextTagOrThrow(inputStream); switch (tag) { - case PKESK: - PublicKeyEncSessionPacket pkeskPacket = (PublicKeyEncSessionPacket) inputStream.readPacket(); - PGPEncryptedDataList encList = null; - break; - case SIG: - automaton.next(PushdownAutomaton.InputAlphabet.Signatures); + case LIT: + automaton.next(InputAlphabet.LiteralData); + PGPLiteralData literalData = new PGPLiteralData(inputStream); + packetLayers.push(Layer.literalMessage(literalData.getDataStream())); + break loop; + case COMP: + automaton.next(InputAlphabet.CompressedData); + PGPCompressedData compressedData = new PGPCompressedData(inputStream); + inputStream = new BCPGInputStream(compressedData.getDataStream()); + packetLayers.push(Layer.compressedData(inputStream)); + break; + + case OPS: + automaton.next(InputAlphabet.OnePassSignatures); ByteArrayOutputStream buf = new ByteArrayOutputStream(); BCPGOutputStream bcpgOut = new BCPGOutputStream(buf); - while (inputStream.nextPacketTag() == PacketTags.SIGNATURE || inputStream.nextPacketTag() == PacketTags.MARKER) { - Packet packet = inputStream.readPacket(); - if (packet instanceof SignaturePacket) { - SignaturePacket sig = (SignaturePacket) packet; - sig.encode(bcpgOut); - } - } - PGPSignatureList signatures = (PGPSignatureList) ImplementationFactory.getInstance() - .getPGPObjectFactory(buf.toByteArray()).nextObject(); - break; - case SKESK: - SymmetricKeyEncSessionPacket skeskPacket = (SymmetricKeyEncSessionPacket) inputStream.readPacket(); - - break; - case OPS: - automaton.next(PushdownAutomaton.InputAlphabet.OnePassSignatures); - buf = new ByteArrayOutputStream(); - bcpgOut = new BCPGOutputStream(buf); while (inputStream.nextPacketTag() == PacketTags.ONE_PASS_SIGNATURE || inputStream.nextPacketTag() == PacketTags.MARKER) { Packet packet = inputStream.readPacket(); if (packet instanceof OnePassSignaturePacket) { @@ -102,60 +102,103 @@ public class PGPDecryptionStream extends InputStream { PGPOnePassSignatureList onePassSignatures = (PGPOnePassSignatureList) ImplementationFactory.getInstance() .getPGPObjectFactory(buf.toByteArray()).nextObject(); break; - case SK: + + case SIG: + automaton.next(InputAlphabet.Signatures); + + buf = new ByteArrayOutputStream(); + bcpgOut = new BCPGOutputStream(buf); + while (inputStream.nextPacketTag() == PacketTags.SIGNATURE || inputStream.nextPacketTag() == PacketTags.MARKER) { + Packet packet = inputStream.readPacket(); + if (packet instanceof SignaturePacket) { + SignaturePacket sig = (SignaturePacket) packet; + sig.encode(bcpgOut); + } + } + PGPSignatureList signatures = (PGPSignatureList) ImplementationFactory.getInstance() + .getPGPObjectFactory(buf.toByteArray()).nextObject(); break; - case PK: + + case PKESK: + PublicKeyEncSessionPacket pkeskPacket = (PublicKeyEncSessionPacket) inputStream.readPacket(); + pkeskList.add(pkeskPacket); break; - case SSK: - break; - case COMP: - automaton.next(PushdownAutomaton.InputAlphabet.CompressedData); - PGPCompressedData compressedData = new PGPCompressedData(inputStream); - inputStream = new BCPGInputStream(compressedData.getDataStream()); - packetLayers.push(Layer.CompressedData(inputStream)); + + case SKESK: + SymmetricKeyEncSessionPacket skeskPacket = (SymmetricKeyEncSessionPacket) inputStream.readPacket(); + skeskList.add(skeskPacket); break; + case SED: - automaton.next(PushdownAutomaton.InputAlphabet.EncryptedData); - SymmetricEncDataPacket symmetricEncDataPacket = (SymmetricEncDataPacket) inputStream.readPacket(); - break; + if (!options.isIgnoreMDCErrors()) { + throw new MessageNotIntegrityProtectedException(); + } + // No break; we continue below! + case SEIPD: + automaton.next(InputAlphabet.EncryptedData); + PGPEncryptedDataList encryptedDataList = assembleEncryptedDataList(inputStream); + + for (PGPEncryptedData encData : encryptedDataList) { + if (encData instanceof PGPPBEEncryptedData) { + PGPPBEEncryptedData skenc = (PGPPBEEncryptedData) encData; + for (Passphrase passphrase : options.getDecryptionPassphrases()) { + PBEDataDecryptorFactory decryptorFactory = ImplementationFactory.getInstance() + .getPBEDataDecryptorFactory(passphrase); + InputStream decryptedIn = skenc.getDataStream(decryptorFactory); + packetLayers.push(Layer.encryptedData(new BCPGInputStream(decryptedIn))); + walkLayer(); + break loop; + } + } + } + throw new MissingDecryptionMethodException("Cannot decrypt message."); + case MARKER: inputStream.readPacket(); // discard break; - case LIT: - automaton.next(PushdownAutomaton.InputAlphabet.LiteralData); - PGPLiteralData literalData = new PGPLiteralData(inputStream); - packetLayers.push(Layer.LiteralMessage(literalData.getDataStream())); - break loop; - case TRUST: - TrustPacket trustPacket = (TrustPacket) inputStream.readPacket(); - break; - case UID: - UserIDPacket userIDPacket = (UserIDPacket) inputStream.readPacket(); - break; + + case SK: + case PK: + case SSK: case PSK: - break; + case TRUST: + case UID: case UATTR: - UserAttributePacket userAttributePacket = (UserAttributePacket) inputStream.readPacket(); - break; - case SEIPD: - automaton.next(PushdownAutomaton.InputAlphabet.EncryptedData); - SymmetricEncIntegrityPacket symmetricEncIntegrityPacket = (SymmetricEncIntegrityPacket) inputStream.readPacket(); - break; + throw new MalformedOpenPgpMessageException("OpenPGP packet " + tag + " MUST NOT be part of OpenPGP messages."); case MOD: ModDetectionCodePacket modDetectionCodePacket = (ModDetectionCodePacket) inputStream.readPacket(); break; case EXP_1: - break; case EXP_2: - break; case EXP_3: - break; case EXP_4: - break; + throw new MalformedOpenPgpMessageException("Experimental packet " + tag + " found inside the message."); } } } + private PGPEncryptedDataList assembleEncryptedDataList(BCPGInputStream inputStream) + throws IOException { + ByteArrayOutputStream buf = new ByteArrayOutputStream(); + BCPGOutputStream bcpgOut = new BCPGOutputStream(buf); + + for (SymmetricKeyEncSessionPacket skesk : skeskList) { + bcpgOut.write(skesk.getEncoded()); + } + skeskList.clear(); + for (PublicKeyEncSessionPacket pkesk : pkeskList) { + bcpgOut.write(pkesk.getEncoded()); + } + pkeskList.clear(); + + SequenceInputStream sqin = new SequenceInputStream( + new ByteArrayInputStream(buf.toByteArray()), inputStream); + + PGPEncryptedDataList encryptedDataList = (PGPEncryptedDataList) ImplementationFactory.getInstance() + .getPGPObjectFactory(sqin).nextObject(); + return encryptedDataList; + } + private OpenPgpPacket nextTagOrThrow(BCPGInputStream inputStream) throws IOException, InvalidOpenPgpPacketException { try { @@ -167,17 +210,13 @@ public class PGPDecryptionStream extends InputStream { private void popLayer() throws MalformedOpenPgpMessageException { if (packetLayers.pop().isNested) - automaton.next(PushdownAutomaton.InputAlphabet.EndOfSequence); + automaton.next(InputAlphabet.EndOfSequence); } @Override public int read() throws IOException { if (packetLayers.isEmpty()) { - try { - automaton.assertValid(); - } catch (MalformedOpenPgpMessageException e) { - throw e.toRuntimeException(); - } + automaton.assertValid(); return -1; } @@ -187,13 +226,10 @@ public class PGPDecryptionStream extends InputStream { } catch (IOException e) { } if (r == -1) { + popLayer(); try { - popLayer(); walkLayer(); - } catch (MalformedOpenPgpMessageException e) { - throw e.toRuntimeException(); - } - catch (PGPException e) { + } catch (PGPException e) { throw new RuntimeException(e); } return read(); @@ -227,11 +263,15 @@ public class PGPDecryptionStream extends InputStream { return new Layer(bcpgIn, true); } - static Layer LiteralMessage(InputStream inputStream) { + static Layer literalMessage(InputStream inputStream) { return new Layer(inputStream, false); } - static Layer CompressedData(InputStream inputStream) { + static Layer compressedData(InputStream inputStream) { + return new Layer(inputStream, true); + } + + static Layer encryptedData(InputStream inputStream) { return new Layer(inputStream, true); } } diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java new file mode 100644 index 00000000..549fe46b --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java @@ -0,0 +1,331 @@ +package org.pgpainless.decryption_verification; + +import com.sun.tools.javac.code.Attribute; +import org.bouncycastle.bcpg.BCPGInputStream; +import org.bouncycastle.bcpg.BCPGOutputStream; +import org.bouncycastle.bcpg.OnePassSignaturePacket; +import org.bouncycastle.bcpg.Packet; +import org.bouncycastle.bcpg.PacketTags; +import org.bouncycastle.bcpg.SignaturePacket; +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; +import org.bouncycastle.openpgp.PGPSecretKey; +import org.bouncycastle.openpgp.PGPSecretKeyRing; +import org.bouncycastle.openpgp.PGPSignature; +import org.bouncycastle.openpgp.PGPSignatureList; +import org.bouncycastle.openpgp.operator.PBEDataDecryptorFactory; +import org.bouncycastle.openpgp.operator.PublicKeyDataDecryptorFactory; +import org.pgpainless.PGPainless; +import org.pgpainless.algorithm.EncryptionPurpose; +import org.pgpainless.algorithm.OpenPgpPacket; +import org.pgpainless.decryption_verification.automaton.InputAlphabet; +import org.pgpainless.decryption_verification.automaton.PDA; +import org.pgpainless.exception.MalformedOpenPgpMessageException; +import org.pgpainless.exception.MessageNotIntegrityProtectedException; +import org.pgpainless.implementation.ImplementationFactory; +import org.pgpainless.key.info.KeyRingInfo; +import org.pgpainless.key.protection.SecretKeyRingProtector; +import org.pgpainless.key.protection.UnlockSecretKey; +import org.pgpainless.util.Passphrase; +import org.pgpainless.util.Tuple; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.List; + +public class OpenPgpMessageInputStream extends InputStream { + + protected final PDA automaton = new PDA(); + protected final ConsumerOptions options; + protected final BCPGInputStream bcpgIn; + protected InputStream in; + + private List signatures = new ArrayList<>(); + private List onePassSignatures = new ArrayList<>(); + + public OpenPgpMessageInputStream(InputStream inputStream, ConsumerOptions options) + throws IOException, PGPException { + this.options = options; + // TODO: Use BCPGInputStream.wrap(inputStream); + if (inputStream instanceof BCPGInputStream) { + this.bcpgIn = (BCPGInputStream) inputStream; + } else { + this.bcpgIn = new BCPGInputStream(inputStream); + } + + walk(); + } + + private void walk() throws IOException, PGPException { + loop: while (true) { + + int tag = bcpgIn.nextPacketTag(); + if (tag == -1) { + break loop; + } + + OpenPgpPacket nextPacket = OpenPgpPacket.requireFromTag(tag); + switch (nextPacket) { + case LIT: + automaton.next(InputAlphabet.LiteralData); + PGPLiteralData literalData = new PGPLiteralData(bcpgIn); + in = literalData.getDataStream(); + break loop; + + case COMP: + automaton.next(InputAlphabet.CompressedData); + PGPCompressedData compressedData = new PGPCompressedData(bcpgIn); + in = new OpenPgpMessageInputStream(compressedData.getDataStream(), options); + break loop; + + case OPS: + automaton.next(InputAlphabet.OnePassSignatures); + readOnePassSignatures(); + break; + + case SIG: + automaton.next(InputAlphabet.Signatures); + readSignatures(); + break; + + case PKESK: + case SKESK: + case SED: + case SEIPD: + automaton.next(InputAlphabet.EncryptedData); + PGPEncryptedDataList encDataList = new PGPEncryptedDataList(bcpgIn); + + // TODO: Replace with !encDataList.isIntegrityProtected() + if (!encDataList.get(0).isIntegrityProtected()) { + throw new MessageNotIntegrityProtectedException(); + } + + SortedESKs esks = new SortedESKs(encDataList); + + // TODO: try session keys + + // Try passwords + for (PGPPBEEncryptedData skesk : esks.skesks) { + for (Passphrase passphrase : options.getDecryptionPassphrases()) { + PBEDataDecryptorFactory decryptorFactory = ImplementationFactory.getInstance() + .getPBEDataDecryptorFactory(passphrase); + try { + InputStream decrypted = skesk.getDataStream(decryptorFactory); + in = new OpenPgpMessageInputStream(decrypted, options); + break loop; + } catch (PGPException e) { + // password mismatch? Try next password + } + + } + } + + // Try (known) secret keys + for (PGPPublicKeyEncryptedData pkesk : esks.pkesks) { + long keyId = pkesk.getKeyID(); + PGPSecretKeyRing decryptionKeys = getDecryptionKey(keyId); + if (decryptionKeys == null) { + continue; + } + SecretKeyRingProtector protector = options.getSecretKeyProtector(decryptionKeys); + PGPSecretKey decryptionKey = decryptionKeys.getSecretKey(keyId); + PGPPrivateKey privateKey = UnlockSecretKey.unlockSecretKey(decryptionKey, protector); + + PublicKeyDataDecryptorFactory decryptorFactory = ImplementationFactory.getInstance() + .getPublicKeyDataDecryptorFactory(privateKey); + try { + InputStream decrypted = pkesk.getDataStream(decryptorFactory); + in = new OpenPgpMessageInputStream(decrypted, options); + break loop; + } catch (PGPException e) { + // hm :/ + } + } + + // try anonymous secret keys + for (PGPPublicKeyEncryptedData pkesk : esks.anonPkesks) { + for (Tuple decryptionKeyCandidate : findPotentialDecryptionKeys(pkesk)) { + SecretKeyRingProtector protector = options.getSecretKeyProtector(decryptionKeyCandidate.getA()); + PGPPrivateKey privateKey = UnlockSecretKey.unlockSecretKey(decryptionKeyCandidate.getB(), protector); + PublicKeyDataDecryptorFactory decryptorFactory = ImplementationFactory.getInstance() + .getPublicKeyDataDecryptorFactory(privateKey); + + try { + InputStream decrypted = pkesk.getDataStream(decryptorFactory); + in = new OpenPgpMessageInputStream(decrypted, options); + break loop; + } catch (PGPException e) { + // hm :/ + } + } + } + + // TODO: try interactive password callbacks + + break loop; + + case MARKER: + bcpgIn.readPacket(); // skip marker packet + break; + + case SK: + case PK: + case SSK: + case PSK: + case TRUST: + case UID: + case UATTR: + + case MOD: + break; + + case EXP_1: + case EXP_2: + case EXP_3: + case EXP_4: + break; + } + } + } + + private List> findPotentialDecryptionKeys(PGPPublicKeyEncryptedData pkesk) { + int algorithm = pkesk.getAlgorithm(); + List> decryptionKeyCandidates = new ArrayList<>(); + + for (PGPSecretKeyRing secretKeys : options.getDecryptionKeys()) { + KeyRingInfo info = PGPainless.inspectKeyRing(secretKeys); + for (PGPPublicKey publicKey : info.getEncryptionSubkeys(EncryptionPurpose.ANY)) { + if (publicKey.getAlgorithm() == algorithm && info.isSecretKeyAvailable(publicKey.getKeyID())) { + PGPSecretKey candidate = secretKeys.getSecretKey(publicKey.getKeyID()); + decryptionKeyCandidates.add(new Tuple<>(secretKeys, candidate)); + } + } + } + return decryptionKeyCandidates; + } + + private PGPSecretKeyRing getDecryptionKey(long keyID) { + for (PGPSecretKeyRing secretKeys : options.getDecryptionKeys()) { + PGPSecretKey decryptionKey = secretKeys.getSecretKey(keyID); + if (decryptionKey == null) { + continue; + } + return secretKeys; + } + return null; + } + + private void readOnePassSignatures() throws IOException { + ByteArrayOutputStream buf = new ByteArrayOutputStream(); + BCPGOutputStream bcpgOut = new BCPGOutputStream(buf); + int tag = bcpgIn.nextPacketTag(); + while (tag == PacketTags.ONE_PASS_SIGNATURE || tag == PacketTags.MARKER) { + Packet packet = bcpgIn.readPacket(); + if (tag == PacketTags.ONE_PASS_SIGNATURE) { + OnePassSignaturePacket sigPacket = (OnePassSignaturePacket) packet; + sigPacket.encode(bcpgOut); + } + } + bcpgOut.close(); + + PGPObjectFactory objectFactory = ImplementationFactory.getInstance().getPGPObjectFactory(buf.toByteArray()); + PGPOnePassSignatureList signatureList = (PGPOnePassSignatureList) objectFactory.nextObject(); + for (PGPOnePassSignature ops : signatureList) { + onePassSignatures.add(ops); + } + } + + private void readSignatures() throws IOException { + ByteArrayOutputStream buf = new ByteArrayOutputStream(); + BCPGOutputStream bcpgOut = new BCPGOutputStream(buf); + int tag = bcpgIn.nextPacketTag(); + while (tag == PacketTags.SIGNATURE || tag == PacketTags.MARKER) { + Packet packet = bcpgIn.readPacket(); + if (tag == PacketTags.SIGNATURE) { + SignaturePacket sigPacket = (SignaturePacket) packet; + sigPacket.encode(bcpgOut); + } + } + bcpgOut.close(); + + PGPObjectFactory objectFactory = ImplementationFactory.getInstance().getPGPObjectFactory(buf.toByteArray()); + PGPSignatureList signatureList = (PGPSignatureList) objectFactory.nextObject(); + for (PGPSignature signature : signatureList) { + signatures.add(signature); + } + } + + @Override + public int read() throws IOException { + int r = -1; + try { + r = in.read(); + } catch (IOException e) { + // + } + if (r == -1) { + if (in instanceof OpenPgpMessageInputStream) { + in.close(); + } else { + try { + walk(); + } catch (PGPException e) { + throw new RuntimeException(e); + } + } + } + return r; + } + + @Override + public void close() throws IOException { + try { + in.close(); + // Nested streams (except LiteralData) need to be closed. + if (automaton.getState() != PDA.State.LiteralMessage) { + automaton.next(InputAlphabet.EndOfSequence); + automaton.assertValid(); + } + } catch (IOException e) { + // + } + + super.close(); + } + + private static class SortedESKs { + + private List skesks = new ArrayList<>(); + private List pkesks = new ArrayList<>(); + private List anonPkesks = new ArrayList<>(); + + SortedESKs(PGPEncryptedDataList esks) { + for (PGPEncryptedData esk : esks) { + if (esk instanceof PGPPBEEncryptedData) { + skesks.add((PGPPBEEncryptedData) esk); + } else if (esk instanceof PGPPublicKeyEncryptedData) { + PGPPublicKeyEncryptedData pkesk = (PGPPublicKeyEncryptedData) esk; + if (pkesk.getKeyID() != 0) { + pkesks.add(pkesk); + } else { + anonPkesks.add(pkesk); + } + } else { + throw new IllegalArgumentException("Unknown ESK class type."); + } + } + } + } +} diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/automaton/InputAlphabet.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/automaton/InputAlphabet.java new file mode 100644 index 00000000..d015a4b3 --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/automaton/InputAlphabet.java @@ -0,0 +1,41 @@ +package org.pgpainless.decryption_verification.automaton; + +import org.bouncycastle.openpgp.PGPCompressedData; +import org.bouncycastle.openpgp.PGPEncryptedDataList; +import org.bouncycastle.openpgp.PGPLiteralData; +import org.bouncycastle.openpgp.PGPOnePassSignatureList; +import org.bouncycastle.openpgp.PGPSignatureList; + +public enum InputAlphabet { + /** + * A {@link PGPLiteralData} packet. + */ + LiteralData, + /** + * A {@link PGPSignatureList} object. + */ + Signatures, + /** + * A {@link PGPOnePassSignatureList} object. + */ + OnePassSignatures, + /** + * A {@link PGPCompressedData} packet. + * The contents of this packet MUST form a valid OpenPGP message, so a nested PDA is opened to verify + * its nested packet sequence. + */ + CompressedData, + /** + * A {@link PGPEncryptedDataList} object. + * This object combines multiple ESKs and the corresponding Symmetrically Encrypted + * (possibly Integrity Protected) Data packet. + */ + EncryptedData, + /** + * Marks the end of a (sub-) sequence. + * This input is given if the end of an OpenPGP message is reached. + * This might be the case for the end of the whole ciphertext, or the end of a packet with nested contents + * (e.g. the end of a Compressed Data packet). + */ + EndOfSequence +} diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/PushdownAutomaton.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/automaton/NestingPDA.java similarity index 78% rename from pgpainless-core/src/main/java/org/pgpainless/decryption_verification/PushdownAutomaton.java rename to pgpainless-core/src/main/java/org/pgpainless/decryption_verification/automaton/NestingPDA.java index 86075280..cf5ee674 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/PushdownAutomaton.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/automaton/NestingPDA.java @@ -1,17 +1,12 @@ -package org.pgpainless.decryption_verification; +package org.pgpainless.decryption_verification.automaton; -import org.bouncycastle.openpgp.PGPCompressedData; -import org.bouncycastle.openpgp.PGPEncryptedDataList; -import org.bouncycastle.openpgp.PGPLiteralData; -import org.bouncycastle.openpgp.PGPOnePassSignatureList; -import org.bouncycastle.openpgp.PGPSignatureList; import org.pgpainless.exception.MalformedOpenPgpMessageException; import java.util.Stack; -import static org.pgpainless.decryption_verification.PushdownAutomaton.StackAlphabet.msg; -import static org.pgpainless.decryption_verification.PushdownAutomaton.StackAlphabet.ops; -import static org.pgpainless.decryption_verification.PushdownAutomaton.StackAlphabet.terminus; +import static org.pgpainless.decryption_verification.automaton.StackAlphabet.msg; +import static org.pgpainless.decryption_verification.automaton.StackAlphabet.ops; +import static org.pgpainless.decryption_verification.automaton.StackAlphabet.terminus; /** * Pushdown Automaton to verify the correct syntax of OpenPGP messages during decryption. @@ -37,71 +32,18 @@ import static org.pgpainless.decryption_verification.PushdownAutomaton.StackAlph * * @see RFC4880 §11.3. OpenPGP Messages */ -public class PushdownAutomaton { - - public enum InputAlphabet { - /** - * A {@link PGPLiteralData} packet. - */ - LiteralData, - /** - * A {@link PGPSignatureList} object. - */ - Signatures, - /** - * A {@link PGPOnePassSignatureList} object. - */ - OnePassSignatures, - /** - * A {@link PGPCompressedData} packet. - * The contents of this packet MUST form a valid OpenPGP message, so a nested PDA is opened to verify - * its nested packet sequence. - */ - CompressedData, - /** - * A {@link PGPEncryptedDataList} object. - * This object combines multiple ESKs and the corresponding Symmetrically Encrypted - * (possibly Integrity Protected) Data packet. - */ - EncryptedData, - /** - * Marks the end of a (sub-) sequence. - * This input is given if the end of an OpenPGP message is reached. - * This might be the case for the end of the whole ciphertext, or the end of a packet with nested contents - * (e.g. the end of a Compressed Data packet). - */ - EndOfSequence - } - - public enum StackAlphabet { - /** - * OpenPGP Message. - */ - msg, - /** - * OnePassSignature (in case of BC this represents a OnePassSignatureList). - */ - ops, - /** - * ESK. Not used, as BC combines encrypted data with their encrypted session keys. - */ - esk, - /** - * Special symbol representing the end of the message. - */ - terminus - } +public class NestingPDA { /** * Set of states of the automaton. - * Each state defines its valid transitions in their {@link State#transition(InputAlphabet, PushdownAutomaton)} + * Each state defines its valid transitions in their {@link State#transition(InputAlphabet, NestingPDA)} * method. */ public enum State { OpenPgpMessage { @Override - State transition(InputAlphabet input, PushdownAutomaton automaton) throws MalformedOpenPgpMessageException { + State transition(InputAlphabet input, NestingPDA automaton) throws MalformedOpenPgpMessageException { StackAlphabet stackItem = automaton.popStack(); if (stackItem != msg) { throw new MalformedOpenPgpMessageException(this, input, stackItem); @@ -135,7 +77,7 @@ public class PushdownAutomaton { LiteralMessage { @Override - State transition(InputAlphabet input, PushdownAutomaton automaton) throws MalformedOpenPgpMessageException { + State transition(InputAlphabet input, NestingPDA automaton) throws MalformedOpenPgpMessageException { StackAlphabet stackItem = automaton.popStack(); switch (input) { @@ -165,7 +107,7 @@ public class PushdownAutomaton { CompressedMessage { @Override - State transition(InputAlphabet input, PushdownAutomaton automaton) throws MalformedOpenPgpMessageException { + State transition(InputAlphabet input, NestingPDA automaton) throws MalformedOpenPgpMessageException { StackAlphabet stackItem = automaton.popStack(); switch (input) { case Signatures: @@ -194,7 +136,7 @@ public class PushdownAutomaton { EncryptedMessage { @Override - State transition(InputAlphabet input, PushdownAutomaton automaton) throws MalformedOpenPgpMessageException { + State transition(InputAlphabet input, NestingPDA automaton) throws MalformedOpenPgpMessageException { StackAlphabet stackItem = automaton.popStack(); switch (input) { case Signatures: @@ -223,7 +165,7 @@ public class PushdownAutomaton { CorrespondingSignature { @Override - State transition(InputAlphabet input, PushdownAutomaton automaton) throws MalformedOpenPgpMessageException { + State transition(InputAlphabet input, NestingPDA automaton) throws MalformedOpenPgpMessageException { StackAlphabet stackItem = automaton.popStack(); if (stackItem == terminus && input == InputAlphabet.EndOfSequence && automaton.stack.isEmpty()) { return Valid; @@ -235,7 +177,7 @@ public class PushdownAutomaton { Valid { @Override - State transition(InputAlphabet input, PushdownAutomaton automaton) throws MalformedOpenPgpMessageException { + State transition(InputAlphabet input, NestingPDA automaton) throws MalformedOpenPgpMessageException { throw new MalformedOpenPgpMessageException(this, input, null); } }, @@ -252,15 +194,15 @@ public class PushdownAutomaton { * @return new state of the automaton * @throws MalformedOpenPgpMessageException in case of an illegal input symbol */ - abstract State transition(InputAlphabet input, PushdownAutomaton automaton) throws MalformedOpenPgpMessageException; + abstract State transition(InputAlphabet input, NestingPDA automaton) throws MalformedOpenPgpMessageException; } private final Stack stack = new Stack<>(); private State state; // Some OpenPGP packets have nested contents (e.g. compressed / encrypted data). - PushdownAutomaton nestedSequence = null; + NestingPDA nestedSequence = null; - public PushdownAutomaton() { + public NestingPDA() { state = State.OpenPgpMessage; stack.push(terminus); stack.push(msg); @@ -301,7 +243,7 @@ public class PushdownAutomaton { // If the processed packet contains nested sequence, open nested automaton for it if (input == InputAlphabet.CompressedData || input == InputAlphabet.EncryptedData) { - nestedSequence = new PushdownAutomaton(); + nestedSequence = new NestingPDA(); } } diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/automaton/PDA.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/automaton/PDA.java new file mode 100644 index 00000000..6b989720 --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/automaton/PDA.java @@ -0,0 +1,237 @@ +package org.pgpainless.decryption_verification.automaton; + +import org.pgpainless.exception.MalformedOpenPgpMessageException; + +import java.util.Stack; + +import static org.pgpainless.decryption_verification.automaton.StackAlphabet.msg; +import static org.pgpainless.decryption_verification.automaton.StackAlphabet.ops; +import static org.pgpainless.decryption_verification.automaton.StackAlphabet.terminus; + +public class PDA { + /** + * Set of states of the automaton. + * Each state defines its valid transitions in their {@link NestingPDA.State#transition(InputAlphabet, NestingPDA)} + * method. + */ + public enum State { + + OpenPgpMessage { + @Override + State transition(InputAlphabet input, PDA automaton) throws MalformedOpenPgpMessageException { + StackAlphabet stackItem = automaton.popStack(); + if (stackItem != msg) { + throw new MalformedOpenPgpMessageException(this, input, stackItem); + } + switch (input) { + + case LiteralData: + return LiteralMessage; + + case Signatures: + automaton.pushStack(msg); + return OpenPgpMessage; + + case OnePassSignatures: + automaton.pushStack(ops); + automaton.pushStack(msg); + return OpenPgpMessage; + + case CompressedData: + return CompressedMessage; + + case EncryptedData: + return EncryptedMessage; + + case EndOfSequence: + default: + throw new MalformedOpenPgpMessageException(this, input, stackItem); + } + } + }, + + LiteralMessage { + @Override + State transition(InputAlphabet input, PDA automaton) throws MalformedOpenPgpMessageException { + StackAlphabet stackItem = automaton.popStack(); + switch (input) { + + case Signatures: + if (stackItem == ops) { + return CorrespondingSignature; + } else { + throw new MalformedOpenPgpMessageException(this, input, stackItem); + } + + case EndOfSequence: + if (stackItem == terminus && automaton.stack.isEmpty()) { + return Valid; + } else { + throw new MalformedOpenPgpMessageException(this, input, stackItem); + } + + case LiteralData: + case OnePassSignatures: + case CompressedData: + case EncryptedData: + default: + throw new MalformedOpenPgpMessageException(this, input, stackItem); + } + } + }, + + CompressedMessage { + @Override + State transition(InputAlphabet input, PDA automaton) throws MalformedOpenPgpMessageException { + StackAlphabet stackItem = automaton.popStack(); + switch (input) { + case Signatures: + if (stackItem == ops) { + return CorrespondingSignature; + } else { + throw new MalformedOpenPgpMessageException(this, input, stackItem); + } + + case EndOfSequence: + if (stackItem == terminus && automaton.stack.isEmpty()) { + return Valid; + } else { + throw new MalformedOpenPgpMessageException(this, input, stackItem); + } + + case LiteralData: + case OnePassSignatures: + case CompressedData: + case EncryptedData: + default: + throw new MalformedOpenPgpMessageException(this, input, stackItem); + } + } + }, + + EncryptedMessage { + @Override + State transition(InputAlphabet input, PDA automaton) throws MalformedOpenPgpMessageException { + StackAlphabet stackItem = automaton.popStack(); + switch (input) { + case Signatures: + if (stackItem == ops) { + return CorrespondingSignature; + } else { + throw new MalformedOpenPgpMessageException(this, input, stackItem); + } + + case EndOfSequence: + if (stackItem == terminus && automaton.stack.isEmpty()) { + return Valid; + } else { + throw new MalformedOpenPgpMessageException(this, input, stackItem); + } + + case LiteralData: + case OnePassSignatures: + case CompressedData: + case EncryptedData: + default: + throw new MalformedOpenPgpMessageException(this, input, stackItem); + } + } + }, + + CorrespondingSignature { + @Override + State transition(InputAlphabet input, PDA automaton) throws MalformedOpenPgpMessageException { + StackAlphabet stackItem = automaton.popStack(); + if (stackItem == terminus && input == InputAlphabet.EndOfSequence && automaton.stack.isEmpty()) { + return Valid; + } else { + throw new MalformedOpenPgpMessageException(this, input, stackItem); + } + } + }, + + Valid { + @Override + State transition(InputAlphabet input, PDA automaton) throws MalformedOpenPgpMessageException { + throw new MalformedOpenPgpMessageException(this, input, null); + } + }, + ; + + /** + * Pop the automatons stack and transition to another state. + * If no valid transition from the current state is available given the popped stack item and input symbol, + * a {@link MalformedOpenPgpMessageException} is thrown. + * Otherwise, the stack is manipulated according to the valid transition and the new state is returned. + * + * @param input input symbol + * @param automaton automaton + * @return new state of the automaton + * @throws MalformedOpenPgpMessageException in case of an illegal input symbol + */ + abstract State transition(InputAlphabet input, PDA automaton) throws MalformedOpenPgpMessageException; + } + + private final Stack stack = new Stack<>(); + private State state; + + public PDA() { + state = State.OpenPgpMessage; + stack.push(terminus); + stack.push(msg); + } + + public void next(InputAlphabet input) throws MalformedOpenPgpMessageException { + State old = state; + StackAlphabet stackItem = stack.isEmpty() ? null : stack.peek(); + state = state.transition(input, this); + System.out.println("Transition from " + old + " to " + state + " via " + input + " with stack " + stackItem); + } + + /** + * Return the current state of the PDA. + * + * @return state + */ + public State getState() { + return state; + } + + /** + * Return true, if the PDA is in a valid state (the OpenPGP message is valid). + * + * @return true if valid, false otherwise + */ + public boolean isValid() { + return getState() == State.Valid && stack.isEmpty(); + } + + public void assertValid() throws MalformedOpenPgpMessageException { + if (!isValid()) { + throw new MalformedOpenPgpMessageException("Pushdown Automaton is not in an acceptable state: " + toString()); + } + } + + /** + * Pop an item from the stack. + * + * @return stack item + */ + private StackAlphabet popStack() { + return stack.pop(); + } + + /** + * Push an item onto the stack. + * + * @param item item + */ + private void pushStack(StackAlphabet item) { + stack.push(item); + } + + @Override + public String toString() { + return "State: " + state + " Stack: " + stack; + } +} diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/automaton/StackAlphabet.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/automaton/StackAlphabet.java new file mode 100644 index 00000000..97dad3d8 --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/automaton/StackAlphabet.java @@ -0,0 +1,20 @@ +package org.pgpainless.decryption_verification.automaton; + +public enum StackAlphabet { + /** + * OpenPGP Message. + */ + msg, + /** + * OnePassSignature (in case of BC this represents a OnePassSignatureList). + */ + ops, + /** + * ESK. Not used, as BC combines encrypted data with their encrypted session keys. + */ + esk, + /** + * Special symbol representing the end of the message. + */ + terminus +} diff --git a/pgpainless-core/src/main/java/org/pgpainless/exception/MalformedOpenPgpMessageException.java b/pgpainless-core/src/main/java/org/pgpainless/exception/MalformedOpenPgpMessageException.java index 9ce2284d..21b6e807 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/exception/MalformedOpenPgpMessageException.java +++ b/pgpainless-core/src/main/java/org/pgpainless/exception/MalformedOpenPgpMessageException.java @@ -4,8 +4,10 @@ package org.pgpainless.exception; -import org.bouncycastle.openpgp.PGPException; -import org.pgpainless.decryption_verification.PushdownAutomaton; +import org.pgpainless.decryption_verification.automaton.InputAlphabet; +import org.pgpainless.decryption_verification.automaton.NestingPDA; +import org.pgpainless.decryption_verification.automaton.PDA; +import org.pgpainless.decryption_verification.automaton.StackAlphabet; /** * Exception that gets thrown if the OpenPGP message is malformed. @@ -13,7 +15,7 @@ import org.pgpainless.decryption_verification.PushdownAutomaton; * * @see RFC4880 §11.3. OpenPGP Messages */ -public class MalformedOpenPgpMessageException extends PGPException { +public class MalformedOpenPgpMessageException extends RuntimeException { public MalformedOpenPgpMessageException(String message) { super(message); @@ -23,20 +25,17 @@ public class MalformedOpenPgpMessageException extends PGPException { super(message, cause); } - public MalformedOpenPgpMessageException(PushdownAutomaton.State state, - PushdownAutomaton.InputAlphabet input, - PushdownAutomaton.StackAlphabet stackItem) { + public MalformedOpenPgpMessageException(NestingPDA.State state, + InputAlphabet input, + StackAlphabet stackItem) { this("Invalid input: There is no legal transition from state '" + state + "' for input '" + input + "' when '" + stackItem + "' is on top of the stack."); } - public RTE toRuntimeException() { - return new RTE(this); + public MalformedOpenPgpMessageException(PDA.State state, InputAlphabet input, StackAlphabet stackItem) { + this("Invalid input: There is no legal transition from state '" + state + "' for input '" + input + "' when '" + stackItem + "' is on top of the stack."); } - public static class RTE extends RuntimeException { - - public RTE(MalformedOpenPgpMessageException e) { - super(e); - } + public MalformedOpenPgpMessageException(String message, PDA automaton) { + super(message + automaton.toString()); } } diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/info/KeyRingInfo.java b/pgpainless-core/src/main/java/org/pgpainless/key/info/KeyRingInfo.java index b818290b..fa4168dd 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/key/info/KeyRingInfo.java +++ b/pgpainless-core/src/main/java/org/pgpainless/key/info/KeyRingInfo.java @@ -1100,29 +1100,33 @@ public class KeyRingInfo { List signingKeys = getSigningSubkeys(); for (PGPPublicKey pk : signingKeys) { - PGPSecretKey sk = getSecretKey(pk.getKeyID()); - if (sk == null) { - // Missing secret key - continue; - } - S2K s2K = sk.getS2K(); - // Unencrypted key - if (s2K == null) { - return true; - } - - // Secret key on smart-card - int s2kType = s2K.getType(); - if (s2kType >= 100 && s2kType <= 110) { - continue; - } - // protected secret key - return true; + return isSecretKeyAvailable(pk.getKeyID()); } // No usable secret key found return false; } + public boolean isSecretKeyAvailable(long keyId) { + PGPSecretKey sk = getSecretKey(keyId); + if (sk == null) { + // Missing secret key + return false; + } + S2K s2K = sk.getS2K(); + // Unencrypted key + if (s2K == null) { + return true; + } + + // Secret key on smart-card + int s2kType = s2K.getType(); + if (s2kType >= 100 && s2kType <= 110) { + return false; + } + // protected secret key + return true; + } + private KeyAccessor getKeyAccessor(@Nullable String userId, long keyID) { if (getPublicKey(keyID) == null) { throw new NoSuchElementException("No subkey with key id " + Long.toHexString(keyID) + " found on this key."); diff --git a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStreamTest.java b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStreamTest.java new file mode 100644 index 00000000..ae6390ce --- /dev/null +++ b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStreamTest.java @@ -0,0 +1,86 @@ +package org.pgpainless.decryption_verification; + +import org.bouncycastle.bcpg.ArmoredInputStream; +import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.util.io.Streams; +import org.junit.jupiter.api.Test; +import org.pgpainless.exception.MalformedOpenPgpMessageException; +import org.pgpainless.util.ArmoredInputStreamFactory; +import org.pgpainless.util.Passphrase; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.pgpainless.decryption_verification.PGPDecryptionStreamTest.COMP; +import static org.pgpainless.decryption_verification.PGPDecryptionStreamTest.COMP_COMP_LIT; +import static org.pgpainless.decryption_verification.PGPDecryptionStreamTest.COMP_LIT; +import static org.pgpainless.decryption_verification.PGPDecryptionStreamTest.LIT; +import static org.pgpainless.decryption_verification.PGPDecryptionStreamTest.LIT_LIT; +import static org.pgpainless.decryption_verification.PGPDecryptionStreamTest.PASSPHRASE; +import static org.pgpainless.decryption_verification.PGPDecryptionStreamTest.PLAINTEXT; +import static org.pgpainless.decryption_verification.PGPDecryptionStreamTest.SENC_LIT; +import static org.pgpainless.decryption_verification.PGPDecryptionStreamTest.SIG_LIT; + +public class OpenPgpMessageInputStreamTest { + + @Test + public void testProcessLIT() throws IOException, PGPException { + String plain = process(LIT, ConsumerOptions.get()); + assertEquals(PLAINTEXT, plain); + } + + @Test + public void testProcessLIT_LIT_fails() { + assertThrows(MalformedOpenPgpMessageException.class, + () -> process(LIT_LIT, ConsumerOptions.get())); + } + + @Test + public void testProcessCOMP_LIT() throws PGPException, IOException { + String plain = process(COMP_LIT, ConsumerOptions.get()); + assertEquals(PLAINTEXT, plain); + } + + @Test + public void testProcessCOMP_fails() { + assertThrows(MalformedOpenPgpMessageException.class, + () -> process(COMP, ConsumerOptions.get())); + } + + @Test + public void testProcessCOMP_COMP_LIT() throws PGPException, IOException { + String plain = process(COMP_COMP_LIT, ConsumerOptions.get()); + assertEquals(PLAINTEXT, plain); + } + + @Test + public void testProcessSIG_LIT() throws PGPException, IOException { + String plain = process(SIG_LIT, ConsumerOptions.get()); + assertEquals(PLAINTEXT, plain); + } + + @Test + public void testProcessSENC_LIT() throws PGPException, IOException { + String plain = process(SENC_LIT, ConsumerOptions.get().addDecryptionPassphrase(Passphrase.fromPassword(PASSPHRASE))); + assertEquals(PLAINTEXT, plain); + } + + private String process(String armoredMessage, ConsumerOptions options) throws PGPException, IOException { + OpenPgpMessageInputStream in = get(armoredMessage, options); + ByteArrayOutputStream out = new ByteArrayOutputStream(); + Streams.pipeAll(in, out); + in.close(); + return out.toString(); + } + + private OpenPgpMessageInputStream get(String armored, ConsumerOptions options) throws IOException, PGPException { + ByteArrayInputStream bytesIn = new ByteArrayInputStream(armored.getBytes(StandardCharsets.UTF_8)); + ArmoredInputStream armorIn = ArmoredInputStreamFactory.get(bytesIn); + OpenPgpMessageInputStream pgpIn = new OpenPgpMessageInputStream(armorIn, options); + return pgpIn; + } +} diff --git a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/PGPDecryptionStreamTest.java b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/PGPDecryptionStreamTest.java index bb2742b5..a84da9d5 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/PGPDecryptionStreamTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/PGPDecryptionStreamTest.java @@ -12,6 +12,8 @@ import org.bouncycastle.openpgp.PGPSignature; import org.bouncycastle.util.io.Streams; import org.junit.jupiter.api.Test; import org.pgpainless.PGPainless; +import org.pgpainless.algorithm.CompressionAlgorithm; +import org.pgpainless.encryption_signing.EncryptionOptions; import org.pgpainless.encryption_signing.EncryptionResult; import org.pgpainless.encryption_signing.EncryptionStream; import org.pgpainless.encryption_signing.ProducerOptions; @@ -19,6 +21,7 @@ import org.pgpainless.encryption_signing.SigningOptions; import org.pgpainless.exception.MalformedOpenPgpMessageException; import org.pgpainless.key.protection.SecretKeyRingProtector; import org.pgpainless.util.ArmoredInputStreamFactory; +import org.pgpainless.util.Passphrase; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; @@ -33,7 +36,7 @@ import static org.junit.jupiter.api.Assertions.assertThrows; public class PGPDecryptionStreamTest { - private static final String KEY = "-----BEGIN PGP PRIVATE KEY BLOCK-----\n" + + public static final String KEY = "-----BEGIN PGP PRIVATE KEY BLOCK-----\n" + "Version: PGPainless\n" + "Comment: DA05 848F 37D4 68E6 F982 C889 7A70 1FC6 904D 3F4C\n" + "Comment: Alice \n" + @@ -58,9 +61,10 @@ public class PGPDecryptionStreamTest { "=THgv\n" + "-----END PGP PRIVATE KEY BLOCK-----"; - private static final String PLAINTEXT = "Hello, World!\n"; + public static final String PLAINTEXT = "Hello, World!\n"; + public static final String PASSPHRASE = "sw0rdf1sh"; - private static final String LIT = "" + + public static final String LIT = "" + "-----BEGIN PGP MESSAGE-----\n" + "Version: PGPainless\n" + "\n" + @@ -68,7 +72,7 @@ public class PGPDecryptionStreamTest { "=WGju\n" + "-----END PGP MESSAGE-----"; - private static final String LIT_LIT = "" + + public static final String LIT_LIT = "" + "-----BEGIN PGP MESSAGE-----\n" + "Version: BCPG v1.71\n" + "\n" + @@ -76,7 +80,7 @@ public class PGPDecryptionStreamTest { "=A91Q\n" + "-----END PGP MESSAGE-----"; - private static final String COMP_LIT = "" + + public static final String COMP_LIT = "" + "-----BEGIN PGP MESSAGE-----\n" + "Version: BCPG v1.71\n" + "\n" + @@ -84,7 +88,7 @@ public class PGPDecryptionStreamTest { "=ZYDg\n" + "-----END PGP MESSAGE-----"; - private static final String COMP = "" + + public static final String COMP = "" + "-----BEGIN PGP MESSAGE-----\n" + "Version: BCPG v1.71\n" + "\n" + @@ -92,7 +96,7 @@ public class PGPDecryptionStreamTest { "=MDzg\n" + "-----END PGP MESSAGE-----"; - private static final String COMP_COMP_LIT = "" + + public static final String COMP_COMP_LIT = "" + "-----BEGIN PGP MESSAGE-----\n" + "Version: BCPG v1.71\n" + "\n" + @@ -101,7 +105,7 @@ public class PGPDecryptionStreamTest { "=K9Zl\n" + "-----END PGP MESSAGE-----"; - private static final String SIG_LIT = "" + + public static final String SIG_LIT = "" + "-----BEGIN PGP MESSAGE-----\n" + "Version: BCPG v1.71\n" + "\n" + @@ -111,6 +115,15 @@ public class PGPDecryptionStreamTest { "=WKPn\n" + "-----END PGP MESSAGE-----"; + public static final String SENC_LIT = "" + + "-----BEGIN PGP MESSAGE-----\n" + + "Version: PGPainless\n" + + "\n" + + "jA0ECQMCuZ0qHNXWnGhg0j8Bdm1cxV65sYb7jDgb4rRMtdNpQ1dC4UpSYuk9YWS2\n" + + "DpNEijbX8b/P1UOK2kJczNDADMRegZuLEI+dNsBnJjk=\n" + + "=i4Y0\n" + + "-----END PGP MESSAGE-----"; + @Test public void genLIT() throws IOException { ArmoredOutputStream armorOut = new ArmoredOutputStream(System.out); @@ -125,7 +138,7 @@ public class PGPDecryptionStreamTest { public void processLIT() throws IOException, PGPException { ByteArrayInputStream bytesIn = new ByteArrayInputStream(LIT.getBytes(StandardCharsets.UTF_8)); ArmoredInputStream armorIn = ArmoredInputStreamFactory.get(bytesIn); - PGPDecryptionStream decIn = new PGPDecryptionStream(armorIn); + MessageDecryptionStream decIn = new MessageDecryptionStream(armorIn, ConsumerOptions.get()); ByteArrayOutputStream out = new ByteArrayOutputStream(); Streams.pipeAll(decIn, out); @@ -152,10 +165,10 @@ public class PGPDecryptionStreamTest { public void processLIT_LIT() throws IOException, PGPException { ByteArrayInputStream bytesIn = new ByteArrayInputStream(LIT_LIT.getBytes(StandardCharsets.UTF_8)); ArmoredInputStream armorIn = ArmoredInputStreamFactory.get(bytesIn); - PGPDecryptionStream decIn = new PGPDecryptionStream(armorIn); + MessageDecryptionStream decIn = new MessageDecryptionStream(armorIn, ConsumerOptions.get()); ByteArrayOutputStream out = new ByteArrayOutputStream(); - assertThrows(MalformedOpenPgpMessageException.RTE.class, () -> Streams.pipeAll(decIn, out)); + assertThrows(MalformedOpenPgpMessageException.class, () -> Streams.pipeAll(decIn, out)); } @Test @@ -175,7 +188,7 @@ public class PGPDecryptionStreamTest { public void processCOMP_LIT() throws IOException, PGPException { ByteArrayInputStream bytesIn = new ByteArrayInputStream(COMP_LIT.getBytes(StandardCharsets.UTF_8)); ArmoredInputStream armorIn = ArmoredInputStreamFactory.get(bytesIn); - PGPDecryptionStream decIn = new PGPDecryptionStream(armorIn); + MessageDecryptionStream decIn = new MessageDecryptionStream(armorIn, ConsumerOptions.get()); ByteArrayOutputStream out = new ByteArrayOutputStream(); Streams.pipeAll(decIn, out); @@ -198,8 +211,8 @@ public class PGPDecryptionStreamTest { public void processCOMP() throws IOException { ByteArrayInputStream bytesIn = new ByteArrayInputStream(COMP.getBytes(StandardCharsets.UTF_8)); ArmoredInputStream armorIn = ArmoredInputStreamFactory.get(bytesIn); - assertThrows(MalformedOpenPgpMessageException.RTE.class, () -> { - PGPDecryptionStream decIn = new PGPDecryptionStream(armorIn); + assertThrows(MalformedOpenPgpMessageException.class, () -> { + MessageDecryptionStream decIn = new MessageDecryptionStream(armorIn, ConsumerOptions.get()); Streams.drain(decIn); }); } @@ -228,7 +241,7 @@ public class PGPDecryptionStreamTest { public void processCOMP_COMP_LIT() throws PGPException, IOException { ByteArrayInputStream bytesIn = new ByteArrayInputStream(COMP_COMP_LIT.getBytes(StandardCharsets.UTF_8)); ArmoredInputStream armorIn = ArmoredInputStreamFactory.get(bytesIn); - PGPDecryptionStream decIn = new PGPDecryptionStream(armorIn); + MessageDecryptionStream decIn = new MessageDecryptionStream(armorIn, ConsumerOptions.get()); ByteArrayOutputStream out = new ByteArrayOutputStream(); Streams.pipeAll(decIn, out); @@ -279,7 +292,35 @@ public class PGPDecryptionStreamTest { public void processSIG_LIT() throws IOException, PGPException { ByteArrayInputStream bytesIn = new ByteArrayInputStream(SIG_LIT.getBytes(StandardCharsets.UTF_8)); ArmoredInputStream armorIn = ArmoredInputStreamFactory.get(bytesIn); - PGPDecryptionStream decIn = new PGPDecryptionStream(armorIn); + MessageDecryptionStream decIn = new MessageDecryptionStream(armorIn, ConsumerOptions.get()); + + ByteArrayOutputStream out = new ByteArrayOutputStream(); + Streams.pipeAll(decIn, out); + decIn.close(); + + System.out.println(out); + } + + @Test + public void genSENC_LIT() throws PGPException, IOException { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + EncryptionStream enc = PGPainless.encryptAndOrSign() + .onOutputStream(out) + .withOptions(ProducerOptions.encrypt(EncryptionOptions.get() + .addPassphrase(Passphrase.fromPassword(PASSPHRASE))) + .overrideCompressionAlgorithm(CompressionAlgorithm.UNCOMPRESSED)); + enc.write(PLAINTEXT.getBytes(StandardCharsets.UTF_8)); + enc.close(); + + System.out.println(out); + } + + @Test + public void processSENC_LIT() throws IOException, PGPException { + ByteArrayInputStream bytesIn = new ByteArrayInputStream(SENC_LIT.getBytes(StandardCharsets.UTF_8)); + ArmoredInputStream armorIn = ArmoredInputStreamFactory.get(bytesIn); + MessageDecryptionStream decIn = new MessageDecryptionStream(armorIn, ConsumerOptions.get() + .addDecryptionPassphrase(Passphrase.fromPassword(PASSPHRASE))); ByteArrayOutputStream out = new ByteArrayOutputStream(); Streams.pipeAll(decIn, out); diff --git a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/PushDownAutomatonTest.java b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/PushDownAutomatonTest.java deleted file mode 100644 index 1bd07308..00000000 --- a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/PushDownAutomatonTest.java +++ /dev/null @@ -1,205 +0,0 @@ -// SPDX-FileCopyrightText: 2022 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.decryption_verification; - -import org.junit.jupiter.api.Test; -import org.pgpainless.exception.MalformedOpenPgpMessageException; - -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.assertTrue; - -public class PushDownAutomatonTest { - - /** - * MSG is valid. - * - * @throws MalformedOpenPgpMessageException fail - */ - @Test - public void testSimpleLiteralMessageIsValid() throws MalformedOpenPgpMessageException { - PushdownAutomaton automaton = new PushdownAutomaton(); - automaton.next(PushdownAutomaton.InputAlphabet.LiteralData); - automaton.next(PushdownAutomaton.InputAlphabet.EndOfSequence); - - assertTrue(automaton.isValid()); - } - - /** - * OPS MSG SIG is valid. - * - * @throws MalformedOpenPgpMessageException fail - */ - @Test - public void testSimpleOpsSignedMesssageIsValid() throws MalformedOpenPgpMessageException { - PushdownAutomaton automaton = new PushdownAutomaton(); - automaton.next(PushdownAutomaton.InputAlphabet.OnePassSignatures); - automaton.next(PushdownAutomaton.InputAlphabet.LiteralData); - automaton.next(PushdownAutomaton.InputAlphabet.Signatures); - automaton.next(PushdownAutomaton.InputAlphabet.EndOfSequence); - - assertTrue(automaton.isValid()); - } - - /** - * SIG MSG is valid. - * - * @throws MalformedOpenPgpMessageException fail - */ - @Test - public void testSimplePrependSignedMessageIsValid() throws MalformedOpenPgpMessageException { - PushdownAutomaton automaton = new PushdownAutomaton(); - automaton.next(PushdownAutomaton.InputAlphabet.Signatures); - automaton.next(PushdownAutomaton.InputAlphabet.LiteralData); - automaton.next(PushdownAutomaton.InputAlphabet.EndOfSequence); - - assertTrue(automaton.isValid()); - } - - /** - * OPS COMP(MSG) SIG is valid. - * - * @throws MalformedOpenPgpMessageException fail - */ - @Test - public void testOPSSignedCompressedMessageIsValid() throws MalformedOpenPgpMessageException { - PushdownAutomaton automaton = new PushdownAutomaton(); - automaton.next(PushdownAutomaton.InputAlphabet.OnePassSignatures); - automaton.next(PushdownAutomaton.InputAlphabet.CompressedData); - automaton.next(PushdownAutomaton.InputAlphabet.LiteralData); - automaton.next(PushdownAutomaton.InputAlphabet.EndOfSequence); - automaton.next(PushdownAutomaton.InputAlphabet.Signatures); - automaton.next(PushdownAutomaton.InputAlphabet.EndOfSequence); - - assertTrue(automaton.isValid()); - } - - /** - * OPS ENC(COMP(COMP(MSG))) SIG is valid. - * - * @throws MalformedOpenPgpMessageException fail - */ - @Test - public void testOpsSignedEncryptedCompressedCompressedMessageIsValid() throws MalformedOpenPgpMessageException { - PushdownAutomaton automaton = new PushdownAutomaton(); - automaton.next(PushdownAutomaton.InputAlphabet.OnePassSignatures); - automaton.next(PushdownAutomaton.InputAlphabet.EncryptedData); - automaton.next(PushdownAutomaton.InputAlphabet.CompressedData); - automaton.next(PushdownAutomaton.InputAlphabet.CompressedData); - - automaton.next(PushdownAutomaton.InputAlphabet.LiteralData); - - automaton.next(PushdownAutomaton.InputAlphabet.EndOfSequence); - automaton.next(PushdownAutomaton.InputAlphabet.EndOfSequence); - automaton.next(PushdownAutomaton.InputAlphabet.EndOfSequence); - automaton.next(PushdownAutomaton.InputAlphabet.Signatures); - automaton.next(PushdownAutomaton.InputAlphabet.EndOfSequence); - - assertTrue(automaton.isValid()); - } - - /** - * MSG SIG is invalid. - * - * @throws MalformedOpenPgpMessageException fail - */ - @Test - public void testLiteralPlusSigsFails() throws MalformedOpenPgpMessageException { - PushdownAutomaton automaton = new PushdownAutomaton(); - automaton.next(PushdownAutomaton.InputAlphabet.LiteralData); - assertThrows(MalformedOpenPgpMessageException.class, - () -> automaton.next(PushdownAutomaton.InputAlphabet.Signatures)); - } - - /** - * MSG MSG is invalid. - * - * @throws MalformedOpenPgpMessageException fail - */ - @Test - public void testTwoLiteralDataPacketsFails() throws MalformedOpenPgpMessageException { - PushdownAutomaton automaton = new PushdownAutomaton(); - automaton.next(PushdownAutomaton.InputAlphabet.LiteralData); - assertThrows(MalformedOpenPgpMessageException.class, - () -> automaton.next(PushdownAutomaton.InputAlphabet.LiteralData)); - } - - /** - * OPS COMP(MSG MSG) SIG is invalid (two literal packets are illegal). - * - * @throws MalformedOpenPgpMessageException fail - */ - @Test - public void testOPSSignedMessageWithTwoLiteralDataPacketsFails() throws MalformedOpenPgpMessageException { - PushdownAutomaton automaton = new PushdownAutomaton(); - automaton.next(PushdownAutomaton.InputAlphabet.OnePassSignatures); - automaton.next(PushdownAutomaton.InputAlphabet.CompressedData); - automaton.next(PushdownAutomaton.InputAlphabet.LiteralData); - assertThrows(MalformedOpenPgpMessageException.class, - () -> automaton.next(PushdownAutomaton.InputAlphabet.LiteralData)); - } - - /** - * OPS COMP(MSG) MSG SIG is invalid. - * - * @throws MalformedOpenPgpMessageException fail - */ - @Test - public void testOPSSignedMessageWithTwoLiteralDataPacketsFails2() throws MalformedOpenPgpMessageException { - PushdownAutomaton automaton = new PushdownAutomaton(); - automaton.next(PushdownAutomaton.InputAlphabet.OnePassSignatures); - automaton.next(PushdownAutomaton.InputAlphabet.CompressedData); - automaton.next(PushdownAutomaton.InputAlphabet.LiteralData); - automaton.next(PushdownAutomaton.InputAlphabet.EndOfSequence); - assertThrows(MalformedOpenPgpMessageException.class, - () -> automaton.next(PushdownAutomaton.InputAlphabet.LiteralData)); - } - - /** - * OPS COMP(MSG SIG) is invalid (MSG SIG does not form valid nested message). - * - * @throws MalformedOpenPgpMessageException fail - */ - @Test - public void testCorrespondingSignaturesOfOpsSignedMessageAreLayerFurtherDownFails() throws MalformedOpenPgpMessageException { - PushdownAutomaton automaton = new PushdownAutomaton(); - automaton.next(PushdownAutomaton.InputAlphabet.OnePassSignatures); - automaton.next(PushdownAutomaton.InputAlphabet.CompressedData); - automaton.next(PushdownAutomaton.InputAlphabet.LiteralData); - assertThrows(MalformedOpenPgpMessageException.class, - () -> automaton.next(PushdownAutomaton.InputAlphabet.Signatures)); - } - - /** - * Empty COMP is invalid. - */ - @Test - public void testEmptyCompressedDataIsInvalid() throws MalformedOpenPgpMessageException { - PushdownAutomaton automaton = new PushdownAutomaton(); - automaton.next(PushdownAutomaton.InputAlphabet.CompressedData); - assertThrows(MalformedOpenPgpMessageException.class, - () -> automaton.next(PushdownAutomaton.InputAlphabet.EndOfSequence)); - } - - @Test - public void testOPSSignedEncryptedCompressedOPSSignedMessageIsValid() throws MalformedOpenPgpMessageException { - PushdownAutomaton automaton = new PushdownAutomaton(); - automaton.next(PushdownAutomaton.InputAlphabet.OnePassSignatures); - - automaton.next(PushdownAutomaton.InputAlphabet.EncryptedData); - automaton.next(PushdownAutomaton.InputAlphabet.OnePassSignatures); - - automaton.next(PushdownAutomaton.InputAlphabet.CompressedData); - automaton.next(PushdownAutomaton.InputAlphabet.LiteralData); - automaton.next(PushdownAutomaton.InputAlphabet.EndOfSequence); - - automaton.next(PushdownAutomaton.InputAlphabet.Signatures); - automaton.next(PushdownAutomaton.InputAlphabet.EndOfSequence); - - automaton.next(PushdownAutomaton.InputAlphabet.Signatures); - automaton.next(PushdownAutomaton.InputAlphabet.EndOfSequence); - - assertTrue(automaton.isValid()); - } -} diff --git a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/automaton/NestingPDATest.java b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/automaton/NestingPDATest.java new file mode 100644 index 00000000..8c1c4921 --- /dev/null +++ b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/automaton/NestingPDATest.java @@ -0,0 +1,205 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.decryption_verification.automaton; + +import org.junit.jupiter.api.Test; +import org.pgpainless.exception.MalformedOpenPgpMessageException; + +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class NestingPDATest { + + /** + * MSG is valid. + * + * @throws MalformedOpenPgpMessageException fail + */ + @Test + public void testSimpleLiteralMessageIsValid() throws MalformedOpenPgpMessageException { + NestingPDA automaton = new NestingPDA(); + automaton.next(InputAlphabet.LiteralData); + automaton.next(InputAlphabet.EndOfSequence); + + assertTrue(automaton.isValid()); + } + + /** + * OPS MSG SIG is valid. + * + * @throws MalformedOpenPgpMessageException fail + */ + @Test + public void testSimpleOpsSignedMesssageIsValid() throws MalformedOpenPgpMessageException { + NestingPDA automaton = new NestingPDA(); + automaton.next(InputAlphabet.OnePassSignatures); + automaton.next(InputAlphabet.LiteralData); + automaton.next(InputAlphabet.Signatures); + automaton.next(InputAlphabet.EndOfSequence); + + assertTrue(automaton.isValid()); + } + + /** + * SIG MSG is valid. + * + * @throws MalformedOpenPgpMessageException fail + */ + @Test + public void testSimplePrependSignedMessageIsValid() throws MalformedOpenPgpMessageException { + NestingPDA automaton = new NestingPDA(); + automaton.next(InputAlphabet.Signatures); + automaton.next(InputAlphabet.LiteralData); + automaton.next(InputAlphabet.EndOfSequence); + + assertTrue(automaton.isValid()); + } + + /** + * OPS COMP(MSG) SIG is valid. + * + * @throws MalformedOpenPgpMessageException fail + */ + @Test + public void testOPSSignedCompressedMessageIsValid() throws MalformedOpenPgpMessageException { + NestingPDA automaton = new NestingPDA(); + automaton.next(InputAlphabet.OnePassSignatures); + automaton.next(InputAlphabet.CompressedData); + automaton.next(InputAlphabet.LiteralData); + automaton.next(InputAlphabet.EndOfSequence); + automaton.next(InputAlphabet.Signatures); + automaton.next(InputAlphabet.EndOfSequence); + + assertTrue(automaton.isValid()); + } + + /** + * OPS ENC(COMP(COMP(MSG))) SIG is valid. + * + * @throws MalformedOpenPgpMessageException fail + */ + @Test + public void testOpsSignedEncryptedCompressedCompressedMessageIsValid() throws MalformedOpenPgpMessageException { + NestingPDA automaton = new NestingPDA(); + automaton.next(InputAlphabet.OnePassSignatures); + automaton.next(InputAlphabet.EncryptedData); + automaton.next(InputAlphabet.CompressedData); + automaton.next(InputAlphabet.CompressedData); + + automaton.next(InputAlphabet.LiteralData); + + automaton.next(InputAlphabet.EndOfSequence); + automaton.next(InputAlphabet.EndOfSequence); + automaton.next(InputAlphabet.EndOfSequence); + automaton.next(InputAlphabet.Signatures); + automaton.next(InputAlphabet.EndOfSequence); + + assertTrue(automaton.isValid()); + } + + /** + * MSG SIG is invalid. + * + * @throws MalformedOpenPgpMessageException fail + */ + @Test + public void testLiteralPlusSigsFails() throws MalformedOpenPgpMessageException { + NestingPDA automaton = new NestingPDA(); + automaton.next(InputAlphabet.LiteralData); + assertThrows(MalformedOpenPgpMessageException.class, + () -> automaton.next(InputAlphabet.Signatures)); + } + + /** + * MSG MSG is invalid. + * + * @throws MalformedOpenPgpMessageException fail + */ + @Test + public void testTwoLiteralDataPacketsFails() throws MalformedOpenPgpMessageException { + NestingPDA automaton = new NestingPDA(); + automaton.next(InputAlphabet.LiteralData); + assertThrows(MalformedOpenPgpMessageException.class, + () -> automaton.next(InputAlphabet.LiteralData)); + } + + /** + * OPS COMP(MSG MSG) SIG is invalid (two literal packets are illegal). + * + * @throws MalformedOpenPgpMessageException fail + */ + @Test + public void testOPSSignedMessageWithTwoLiteralDataPacketsFails() throws MalformedOpenPgpMessageException { + NestingPDA automaton = new NestingPDA(); + automaton.next(InputAlphabet.OnePassSignatures); + automaton.next(InputAlphabet.CompressedData); + automaton.next(InputAlphabet.LiteralData); + assertThrows(MalformedOpenPgpMessageException.class, + () -> automaton.next(InputAlphabet.LiteralData)); + } + + /** + * OPS COMP(MSG) MSG SIG is invalid. + * + * @throws MalformedOpenPgpMessageException fail + */ + @Test + public void testOPSSignedMessageWithTwoLiteralDataPacketsFails2() throws MalformedOpenPgpMessageException { + NestingPDA automaton = new NestingPDA(); + automaton.next(InputAlphabet.OnePassSignatures); + automaton.next(InputAlphabet.CompressedData); + automaton.next(InputAlphabet.LiteralData); + automaton.next(InputAlphabet.EndOfSequence); + assertThrows(MalformedOpenPgpMessageException.class, + () -> automaton.next(InputAlphabet.LiteralData)); + } + + /** + * OPS COMP(MSG SIG) is invalid (MSG SIG does not form valid nested message). + * + * @throws MalformedOpenPgpMessageException fail + */ + @Test + public void testCorrespondingSignaturesOfOpsSignedMessageAreLayerFurtherDownFails() throws MalformedOpenPgpMessageException { + NestingPDA automaton = new NestingPDA(); + automaton.next(InputAlphabet.OnePassSignatures); + automaton.next(InputAlphabet.CompressedData); + automaton.next(InputAlphabet.LiteralData); + assertThrows(MalformedOpenPgpMessageException.class, + () -> automaton.next(InputAlphabet.Signatures)); + } + + /** + * Empty COMP is invalid. + */ + @Test + public void testEmptyCompressedDataIsInvalid() throws MalformedOpenPgpMessageException { + NestingPDA automaton = new NestingPDA(); + automaton.next(InputAlphabet.CompressedData); + assertThrows(MalformedOpenPgpMessageException.class, + () -> automaton.next(InputAlphabet.EndOfSequence)); + } + + @Test + public void testOPSSignedEncryptedCompressedOPSSignedMessageIsValid() throws MalformedOpenPgpMessageException { + NestingPDA automaton = new NestingPDA(); + automaton.next(InputAlphabet.OnePassSignatures); + + automaton.next(InputAlphabet.EncryptedData); + automaton.next(InputAlphabet.OnePassSignatures); + + automaton.next(InputAlphabet.CompressedData); + automaton.next(InputAlphabet.LiteralData); + automaton.next(InputAlphabet.EndOfSequence); + + automaton.next(InputAlphabet.Signatures); + automaton.next(InputAlphabet.EndOfSequence); + + automaton.next(InputAlphabet.Signatures); + automaton.next(InputAlphabet.EndOfSequence); + + assertTrue(automaton.isValid()); + } +} diff --git a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/automaton/PDATest.java b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/automaton/PDATest.java new file mode 100644 index 00000000..6e8a38d6 --- /dev/null +++ b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/automaton/PDATest.java @@ -0,0 +1,75 @@ +package org.pgpainless.decryption_verification.automaton; + +import org.junit.jupiter.api.Test; +import org.pgpainless.exception.MalformedOpenPgpMessageException; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class PDATest { + + + /** + * MSG is valid. + * + * @throws MalformedOpenPgpMessageException fail + */ + @Test + public void testSimpleLiteralMessageIsValid() throws MalformedOpenPgpMessageException { + PDA automaton = new PDA(); + automaton.next(InputAlphabet.LiteralData); + automaton.next(InputAlphabet.EndOfSequence); + + assertTrue(automaton.isValid()); + } + + /** + * OPS MSG SIG is valid. + * + * @throws MalformedOpenPgpMessageException fail + */ + @Test + public void testSimpleOpsSignedMesssageIsValid() throws MalformedOpenPgpMessageException { + PDA automaton = new PDA(); + automaton.next(InputAlphabet.OnePassSignatures); + automaton.next(InputAlphabet.LiteralData); + automaton.next(InputAlphabet.Signatures); + automaton.next(InputAlphabet.EndOfSequence); + + assertTrue(automaton.isValid()); + } + + + /** + * SIG MSG is valid. + * + * @throws MalformedOpenPgpMessageException fail + */ + @Test + public void testSimplePrependSignedMessageIsValid() throws MalformedOpenPgpMessageException { + PDA automaton = new PDA(); + automaton.next(InputAlphabet.Signatures); + automaton.next(InputAlphabet.LiteralData); + automaton.next(InputAlphabet.EndOfSequence); + + assertTrue(automaton.isValid()); + } + + + /** + * OPS COMP(MSG) SIG is valid. + * + * @throws MalformedOpenPgpMessageException fail + */ + @Test + public void testOPSSignedCompressedMessageIsValid() throws MalformedOpenPgpMessageException { + PDA automaton = new PDA(); + automaton.next(InputAlphabet.OnePassSignatures); + automaton.next(InputAlphabet.CompressedData); + // Here would be a nested PDA for the LiteralData packet + automaton.next(InputAlphabet.Signatures); + automaton.next(InputAlphabet.EndOfSequence); + + assertTrue(automaton.isValid()); + } + +} From d81c0d44006d1037b8d577869dd3a49cbf494872 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 13 Sep 2022 20:22:31 +0200 Subject: [PATCH 017/763] Fix tests --- .../OpenPgpMessageInputStream.java | 57 ++++++++++++++++--- .../automaton/PDA.java | 9 ++- .../OpenPgpMessageInputStreamTest.java | 12 ++++ .../PGPDecryptionStreamTest.java | 30 ++++++++++ 4 files changed, 98 insertions(+), 10 deletions(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java index 549fe46b..788be4ab 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java @@ -1,6 +1,5 @@ package org.pgpainless.decryption_verification; -import com.sun.tools.javac.code.Attribute; import org.bouncycastle.bcpg.BCPGInputStream; import org.bouncycastle.bcpg.BCPGOutputStream; import org.bouncycastle.bcpg.OnePassSignaturePacket; @@ -25,13 +24,14 @@ import org.bouncycastle.openpgp.PGPSignature; import org.bouncycastle.openpgp.PGPSignatureList; import org.bouncycastle.openpgp.operator.PBEDataDecryptorFactory; import org.bouncycastle.openpgp.operator.PublicKeyDataDecryptorFactory; +import org.bouncycastle.openpgp.operator.SessionKeyDataDecryptorFactory; import org.pgpainless.PGPainless; import org.pgpainless.algorithm.EncryptionPurpose; import org.pgpainless.algorithm.OpenPgpPacket; import org.pgpainless.decryption_verification.automaton.InputAlphabet; import org.pgpainless.decryption_verification.automaton.PDA; -import org.pgpainless.exception.MalformedOpenPgpMessageException; import org.pgpainless.exception.MessageNotIntegrityProtectedException; +import org.pgpainless.exception.MissingDecryptionMethodException; import org.pgpainless.implementation.ImplementationFactory; import org.pgpainless.key.info.KeyRingInfo; import org.pgpainless.key.protection.SecretKeyRingProtector; @@ -114,7 +114,27 @@ public class OpenPgpMessageInputStream extends InputStream { SortedESKs esks = new SortedESKs(encDataList); - // TODO: try session keys + if (options.getSessionKey() != null) { + SessionKeyDataDecryptorFactory decryptorFactory = ImplementationFactory.getInstance() + .getSessionKeyDataDecryptorFactory(options.getSessionKey()); + // TODO: Replace with encDataList.addSessionKeyDecryptionMethod(sessionKey) + PGPEncryptedData esk = esks.all().get(0); + try { + if (esk instanceof PGPPBEEncryptedData) { + PGPPBEEncryptedData skesk = (PGPPBEEncryptedData) esk; + in = skesk.getDataStream(decryptorFactory); + break loop; + } else if (esk instanceof PGPPublicKeyEncryptedData) { + PGPPublicKeyEncryptedData pkesk = (PGPPublicKeyEncryptedData) esk; + in = pkesk.getDataStream(decryptorFactory); + break loop; + } else { + throw new RuntimeException("Unknown ESK class type: " + esk.getClass().getName()); + } + } catch (PGPException e) { + // Session key mismatch? + } + } // Try passwords for (PGPPBEEncryptedData skesk : esks.skesks) { @@ -174,7 +194,7 @@ public class OpenPgpMessageInputStream extends InputStream { // TODO: try interactive password callbacks - break loop; + throw new MissingDecryptionMethodException("No working decryption method found."); case MARKER: bcpgIn.readPacket(); // skip marker packet @@ -256,6 +276,7 @@ public class OpenPgpMessageInputStream extends InputStream { if (tag == PacketTags.SIGNATURE) { SignaturePacket sigPacket = (SignaturePacket) packet; sigPacket.encode(bcpgOut); + tag = bcpgIn.nextPacketTag(); } } bcpgOut.close(); @@ -270,16 +291,21 @@ public class OpenPgpMessageInputStream extends InputStream { @Override public int read() throws IOException { int r = -1; - try { - r = in.read(); - } catch (IOException e) { - // + if (in != null) { + try { + r = in.read(); + } catch (IOException e) { + // + } } if (r == -1) { if (in instanceof OpenPgpMessageInputStream) { + System.out.println("Read -1: close " + automaton); in.close(); + in = null; } else { try { + System.out.println("Walk " + automaton); walk(); } catch (PGPException e) { throw new RuntimeException(e); @@ -291,8 +317,15 @@ public class OpenPgpMessageInputStream extends InputStream { @Override public void close() throws IOException { + if (in == null) { + System.out.println("Close " + automaton); + automaton.next(InputAlphabet.EndOfSequence); + automaton.assertValid(); + return; + } try { in.close(); + in = null; // Nested streams (except LiteralData) need to be closed. if (automaton.getState() != PDA.State.LiteralMessage) { automaton.next(InputAlphabet.EndOfSequence); @@ -327,5 +360,13 @@ public class OpenPgpMessageInputStream extends InputStream { } } } + + public List all() { + List esks = new ArrayList<>(); + esks.addAll(skesks); + esks.addAll(pkesks); + esks.addAll(anonPkesks); + return esks; + } } } diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/automaton/PDA.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/automaton/PDA.java index 6b989720..a3384070 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/automaton/PDA.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/automaton/PDA.java @@ -9,6 +9,9 @@ import static org.pgpainless.decryption_verification.automaton.StackAlphabet.ops import static org.pgpainless.decryption_verification.automaton.StackAlphabet.terminus; public class PDA { + + private static int ID = 0; + /** * Set of states of the automaton. * Each state defines its valid transitions in their {@link NestingPDA.State#transition(InputAlphabet, NestingPDA)} @@ -174,18 +177,20 @@ public class PDA { private final Stack stack = new Stack<>(); private State state; + private int id; public PDA() { state = State.OpenPgpMessage; stack.push(terminus); stack.push(msg); + this.id = ID++; } public void next(InputAlphabet input) throws MalformedOpenPgpMessageException { State old = state; StackAlphabet stackItem = stack.isEmpty() ? null : stack.peek(); state = state.transition(input, this); - System.out.println("Transition from " + old + " to " + state + " via " + input + " with stack " + stackItem); + System.out.println(id + ": Transition from " + old + " to " + state + " via " + input + " with stack " + stackItem); } /** @@ -232,6 +237,6 @@ public class PDA { @Override public String toString() { - return "State: " + state + " Stack: " + stack; + return "PDA " + id + ": State: " + state + " Stack: " + stack; } } diff --git a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStreamTest.java b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStreamTest.java index ae6390ce..5b42101c 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStreamTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStreamTest.java @@ -2,8 +2,10 @@ package org.pgpainless.decryption_verification; import org.bouncycastle.bcpg.ArmoredInputStream; import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.PGPSecretKeyRing; import org.bouncycastle.util.io.Streams; import org.junit.jupiter.api.Test; +import org.pgpainless.PGPainless; import org.pgpainless.exception.MalformedOpenPgpMessageException; import org.pgpainless.util.ArmoredInputStreamFactory; import org.pgpainless.util.Passphrase; @@ -18,9 +20,11 @@ import static org.junit.jupiter.api.Assertions.assertThrows; import static org.pgpainless.decryption_verification.PGPDecryptionStreamTest.COMP; import static org.pgpainless.decryption_verification.PGPDecryptionStreamTest.COMP_COMP_LIT; import static org.pgpainless.decryption_verification.PGPDecryptionStreamTest.COMP_LIT; +import static org.pgpainless.decryption_verification.PGPDecryptionStreamTest.KEY; import static org.pgpainless.decryption_verification.PGPDecryptionStreamTest.LIT; import static org.pgpainless.decryption_verification.PGPDecryptionStreamTest.LIT_LIT; import static org.pgpainless.decryption_verification.PGPDecryptionStreamTest.PASSPHRASE; +import static org.pgpainless.decryption_verification.PGPDecryptionStreamTest.PENC_COMP_LIT; import static org.pgpainless.decryption_verification.PGPDecryptionStreamTest.PLAINTEXT; import static org.pgpainless.decryption_verification.PGPDecryptionStreamTest.SENC_LIT; import static org.pgpainless.decryption_verification.PGPDecryptionStreamTest.SIG_LIT; @@ -69,6 +73,14 @@ public class OpenPgpMessageInputStreamTest { assertEquals(PLAINTEXT, plain); } + @Test + public void testProcessPENC_COMP_LIT() throws IOException, PGPException { + PGPSecretKeyRing secretKeys = PGPainless.readKeyRing().secretKeyRing(KEY); + String plain = process(PENC_COMP_LIT, ConsumerOptions.get() + .addDecryptionKey(secretKeys)); + assertEquals(PLAINTEXT, plain); + } + private String process(String armoredMessage, ConsumerOptions options) throws PGPException, IOException { OpenPgpMessageInputStream in = get(armoredMessage, options); ByteArrayOutputStream out = new ByteArrayOutputStream(); diff --git a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/PGPDecryptionStreamTest.java b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/PGPDecryptionStreamTest.java index a84da9d5..8da44ec8 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/PGPDecryptionStreamTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/PGPDecryptionStreamTest.java @@ -7,6 +7,7 @@ import org.bouncycastle.openpgp.PGPCompressedDataGenerator; import org.bouncycastle.openpgp.PGPException; import org.bouncycastle.openpgp.PGPLiteralData; import org.bouncycastle.openpgp.PGPLiteralDataGenerator; +import org.bouncycastle.openpgp.PGPPublicKeyRing; import org.bouncycastle.openpgp.PGPSecretKeyRing; import org.bouncycastle.openpgp.PGPSignature; import org.bouncycastle.util.io.Streams; @@ -124,6 +125,17 @@ public class PGPDecryptionStreamTest { "=i4Y0\n" + "-----END PGP MESSAGE-----"; + public static final String PENC_COMP_LIT = "" + + "-----BEGIN PGP MESSAGE-----\n" + + "Version: PGPainless\n" + + "\n" + + "hF4Dyqa/GWUy6WsSAQdAQ62BwmUt8Iby0+jvrLhMgST79KR/as+dyl0nf1uki2sw\n" + + "Thg1Ojtf0hOyJgcpQ4nP2Q0wYFR0F1sCydaIlTGreYZHlGtybP7/Ml6KNZILTRWP\n" + + "0kYBkGBgK7oQWRIVyoF2POvEP6EX1X8nvQk7O3NysVdRVbnia7gE3AzRYuha4kxs\n" + + "pI6xJkntLMS3K6him1Y9FHINIASFSB+C\n" + + "=5p00\n" + + "-----END PGP MESSAGE-----"; + @Test public void genLIT() throws IOException { ArmoredOutputStream armorOut = new ArmoredOutputStream(System.out); @@ -328,4 +340,22 @@ public class PGPDecryptionStreamTest { System.out.println(out); } + + @Test + public void genPENC_COMP_LIT() throws IOException, PGPException { + PGPSecretKeyRing secretKeys = PGPainless.readKeyRing().secretKeyRing(KEY); + PGPPublicKeyRing cert = PGPainless.extractCertificate(secretKeys); + + ByteArrayOutputStream out = new ByteArrayOutputStream(); + EncryptionStream enc = PGPainless.encryptAndOrSign() + .onOutputStream(out) + .withOptions(ProducerOptions.encrypt(EncryptionOptions.get() + .addRecipient(cert)) + .overrideCompressionAlgorithm(CompressionAlgorithm.ZLIB)); + + Streams.pipeAll(new ByteArrayInputStream(PLAINTEXT.getBytes(StandardCharsets.UTF_8)), enc); + enc.close(); + + System.out.println(out); + } } From 0753f4d38acbc0bf1f2257496871ffa26734b0e9 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 14 Sep 2022 19:29:47 +0200 Subject: [PATCH 018/763] Work on getting signature verification to function again --- .../OpenPgpMessageInputStream.java | 145 +++++++++++++----- .../automaton/PDA.java | 7 + 2 files changed, 116 insertions(+), 36 deletions(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java index 788be4ab..e95c9ff7 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java @@ -30,19 +30,24 @@ import org.pgpainless.algorithm.EncryptionPurpose; import org.pgpainless.algorithm.OpenPgpPacket; import org.pgpainless.decryption_verification.automaton.InputAlphabet; import org.pgpainless.decryption_verification.automaton.PDA; +import org.pgpainless.decryption_verification.automaton.StackAlphabet; +import org.pgpainless.exception.MalformedOpenPgpMessageException; import org.pgpainless.exception.MessageNotIntegrityProtectedException; import org.pgpainless.exception.MissingDecryptionMethodException; import org.pgpainless.implementation.ImplementationFactory; import org.pgpainless.key.info.KeyRingInfo; import org.pgpainless.key.protection.SecretKeyRingProtector; import org.pgpainless.key.protection.UnlockSecretKey; +import org.pgpainless.signature.consumer.DetachedSignatureCheck; import org.pgpainless.util.Passphrase; import org.pgpainless.util.Tuple; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; +import java.io.UnsupportedEncodingException; import java.util.ArrayList; +import java.util.Collection; import java.util.List; public class OpenPgpMessageInputStream extends InputStream { @@ -52,12 +57,12 @@ public class OpenPgpMessageInputStream extends InputStream { protected final BCPGInputStream bcpgIn; protected InputStream in; - private List signatures = new ArrayList<>(); - private List onePassSignatures = new ArrayList<>(); + private boolean closed = false; + + private Signatures signatures = new Signatures(); public OpenPgpMessageInputStream(InputStream inputStream, ConsumerOptions options) throws IOException, PGPException { - this.options = options; // TODO: Use BCPGInputStream.wrap(inputStream); if (inputStream instanceof BCPGInputStream) { this.bcpgIn = (BCPGInputStream) inputStream; @@ -65,41 +70,61 @@ public class OpenPgpMessageInputStream extends InputStream { this.bcpgIn = new BCPGInputStream(inputStream); } - walk(); + this.options = options; + this.signatures.addDetachedSignatures(options.getDetachedSignatures()); + + consumePackets(); } - private void walk() throws IOException, PGPException { - loop: while (true) { - - int tag = bcpgIn.nextPacketTag(); - if (tag == -1) { - break loop; - } - + /** + * This method consumes OpenPGP packets from the current {@link BCPGInputStream}. + * Once it reaches a "nested" OpenPGP packet (Literal Data, Compressed Data, Encrypted Data), it sets

in
+ * to the nested stream and breaks the loop. + * The nested stream is either a simple {@link InputStream} (in case of Literal Data), or another + * {@link OpenPgpMessageInputStream} in case of Compressed and Encrypted Data. + * + * @throws IOException + * @throws PGPException + */ + private void consumePackets() + throws IOException, PGPException { + int tag; + loop: while ((tag = bcpgIn.nextPacketTag()) != -1) { OpenPgpPacket nextPacket = OpenPgpPacket.requireFromTag(tag); switch (nextPacket) { + + // Literal Data - the literal data content is the new input stream case LIT: automaton.next(InputAlphabet.LiteralData); PGPLiteralData literalData = new PGPLiteralData(bcpgIn); in = literalData.getDataStream(); break loop; + // Compressed Data - the content contains another OpenPGP message case COMP: automaton.next(InputAlphabet.CompressedData); PGPCompressedData compressedData = new PGPCompressedData(bcpgIn); in = new OpenPgpMessageInputStream(compressedData.getDataStream(), options); break loop; + // One Pass Signatures case OPS: automaton.next(InputAlphabet.OnePassSignatures); - readOnePassSignatures(); + signatures.addOnePassSignatures(readOnePassSignatures()); break; + // Signatures - either prepended to the message, or corresponding to the One Pass Signatures case SIG: automaton.next(InputAlphabet.Signatures); - readSignatures(); + PGPSignatureList signatureList = readSignatures(); + if (automaton.peekStack() == StackAlphabet.ops) { + signatures.addOnePassCorrespondingSignatures(signatureList); + } else { + signatures.addPrependedSignatures(signatureList); + } break; + // Encrypted Data (ESKs and SED/SEIPD are parsed the same by BC) case PKESK: case SKESK: case SED: @@ -200,6 +225,7 @@ public class OpenPgpMessageInputStream extends InputStream { bcpgIn.readPacket(); // skip marker packet break; + // Key Packets are illegal in this context case SK: case PK: case SSK: @@ -209,13 +235,14 @@ public class OpenPgpMessageInputStream extends InputStream { case UATTR: case MOD: - break; + throw new MalformedOpenPgpMessageException("Unexpected Packet in Stream: " + nextPacket); + // Experimental Packets are not supported case EXP_1: case EXP_2: case EXP_3: case EXP_4: - break; + throw new MalformedOpenPgpMessageException("Unsupported Packet in Stream: " + nextPacket); } } } @@ -247,7 +274,7 @@ public class OpenPgpMessageInputStream extends InputStream { return null; } - private void readOnePassSignatures() throws IOException { + private PGPOnePassSignatureList readOnePassSignatures() throws IOException { ByteArrayOutputStream buf = new ByteArrayOutputStream(); BCPGOutputStream bcpgOut = new BCPGOutputStream(buf); int tag = bcpgIn.nextPacketTag(); @@ -262,12 +289,10 @@ public class OpenPgpMessageInputStream extends InputStream { PGPObjectFactory objectFactory = ImplementationFactory.getInstance().getPGPObjectFactory(buf.toByteArray()); PGPOnePassSignatureList signatureList = (PGPOnePassSignatureList) objectFactory.nextObject(); - for (PGPOnePassSignature ops : signatureList) { - onePassSignatures.add(ops); - } + return signatureList; } - private void readSignatures() throws IOException { + private PGPSignatureList readSignatures() throws IOException { ByteArrayOutputStream buf = new ByteArrayOutputStream(); BCPGOutputStream bcpgOut = new BCPGOutputStream(buf); int tag = bcpgIn.nextPacketTag(); @@ -283,9 +308,7 @@ public class OpenPgpMessageInputStream extends InputStream { PGPObjectFactory objectFactory = ImplementationFactory.getInstance().getPGPObjectFactory(buf.toByteArray()); PGPSignatureList signatureList = (PGPSignatureList) objectFactory.nextObject(); - for (PGPSignature signature : signatureList) { - signatures.add(signature); - } + return signatureList; } @Override @@ -298,15 +321,17 @@ public class OpenPgpMessageInputStream extends InputStream { // } } - if (r == -1) { + if (r != -1) { + byte b = (byte) r; + signatures.update(b); + } else { if (in instanceof OpenPgpMessageInputStream) { - System.out.println("Read -1: close " + automaton); in.close(); in = null; } else { try { System.out.println("Walk " + automaton); - walk(); + consumePackets(); } catch (PGPException e) { throw new RuntimeException(e); } @@ -317,25 +342,24 @@ public class OpenPgpMessageInputStream extends InputStream { @Override public void close() throws IOException { - if (in == null) { - System.out.println("Close " + automaton); - automaton.next(InputAlphabet.EndOfSequence); - automaton.assertValid(); + if (closed) { return; } - try { + + if (in != null) { in.close(); - in = null; - // Nested streams (except LiteralData) need to be closed. + in = null; // TODO: Collect result of in before nulling if (automaton.getState() != PDA.State.LiteralMessage) { automaton.next(InputAlphabet.EndOfSequence); automaton.assertValid(); } - } catch (IOException e) { - // + } else { + automaton.next(InputAlphabet.EndOfSequence); + automaton.assertValid(); } super.close(); + closed = true; } private static class SortedESKs { @@ -369,4 +393,53 @@ public class OpenPgpMessageInputStream extends InputStream { return esks; } } + + private static class Signatures { + List detachedSignatures = new ArrayList<>(); + List prependedSignatures = new ArrayList<>(); + List onePassSignatures = new ArrayList<>(); + List correspondingSignatures = new ArrayList<>(); + + void addDetachedSignatures(Collection signatures) { + this.detachedSignatures.addAll(signatures); + } + + void addPrependedSignatures(PGPSignatureList signatures) { + for (PGPSignature signature : signatures) { + this.prependedSignatures.add(signature); + } + } + + void addOnePassSignatures(PGPOnePassSignatureList signatures) { + for (PGPOnePassSignature ops : signatures) { + this.onePassSignatures.add(ops); + } + } + + void addOnePassCorrespondingSignatures(PGPSignatureList signatures) { + for (PGPSignature signature : signatures) { + correspondingSignatures.add(signature); + } + } + + public void update(byte b) { + /** + for (PGPSignature prepended : prependedSignatures) { + prepended.update(b); + } + for (PGPOnePassSignature ops : onePassSignatures) { + ops.update(b); + } + for (PGPSignature detached : detachedSignatures) { + detached.update(b); + } + */ + } + + public void finish() { + for (PGPSignature detached : detachedSignatures) { + + } + } + } } diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/automaton/PDA.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/automaton/PDA.java index a3384070..793a3451 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/automaton/PDA.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/automaton/PDA.java @@ -202,6 +202,13 @@ public class PDA { return state; } + public StackAlphabet peekStack() { + if (stack.isEmpty()) { + return null; + } + return stack.peek(); + } + /** * Return true, if the PDA is in a valid state (the OpenPGP message is valid). * From 7b9db97212eb541ffa05ad026bdb49214103de17 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 14 Sep 2022 19:41:22 +0200 Subject: [PATCH 019/763] Clean close() method --- .../OpenPgpMessageInputStream.java | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java index e95c9ff7..d95e3e3c 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java @@ -348,17 +348,11 @@ public class OpenPgpMessageInputStream extends InputStream { if (in != null) { in.close(); - in = null; // TODO: Collect result of in before nulling - if (automaton.getState() != PDA.State.LiteralMessage) { - automaton.next(InputAlphabet.EndOfSequence); - automaton.assertValid(); - } - } else { - automaton.next(InputAlphabet.EndOfSequence); - automaton.assertValid(); + in = null; } - super.close(); + automaton.next(InputAlphabet.EndOfSequence); + automaton.assertValid(); closed = true; } From 9366700895de58a8d4903718e515f4866888fe60 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 14 Sep 2022 20:10:42 +0200 Subject: [PATCH 020/763] Add read(b,off,len) --- .../OpenPgpMessageInputStream.java | 71 +++++++++++++------ 1 file changed, 50 insertions(+), 21 deletions(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java index d95e3e3c..4c6a85c8 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java @@ -38,14 +38,12 @@ import org.pgpainless.implementation.ImplementationFactory; import org.pgpainless.key.info.KeyRingInfo; import org.pgpainless.key.protection.SecretKeyRingProtector; import org.pgpainless.key.protection.UnlockSecretKey; -import org.pgpainless.signature.consumer.DetachedSignatureCheck; import org.pgpainless.util.Passphrase; import org.pgpainless.util.Tuple; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; -import java.io.UnsupportedEncodingException; import java.util.ArrayList; import java.util.Collection; import java.util.List; @@ -88,6 +86,7 @@ public class OpenPgpMessageInputStream extends InputStream { */ private void consumePackets() throws IOException, PGPException { + System.out.println("Walk " + automaton); int tag; loop: while ((tag = bcpgIn.nextPacketTag()) != -1) { OpenPgpPacket nextPacket = OpenPgpPacket.requireFromTag(tag); @@ -237,7 +236,7 @@ public class OpenPgpMessageInputStream extends InputStream { case MOD: throw new MalformedOpenPgpMessageException("Unexpected Packet in Stream: " + nextPacket); - // Experimental Packets are not supported + // Experimental Packets are not supported case EXP_1: case EXP_2: case EXP_3: @@ -313,15 +312,19 @@ public class OpenPgpMessageInputStream extends InputStream { @Override public int read() throws IOException { - int r = -1; - if (in != null) { - try { - r = in.read(); - } catch (IOException e) { - // - } + if (in == null) { + automaton.assertValid(); + return -1; } - if (r != -1) { + + int r; + try { + r = in.read(); + } catch (IOException e) { + r = -1; + } + boolean eos = r == -1; + if (!eos) { byte b = (byte) r; signatures.update(b); } else { @@ -330,7 +333,32 @@ public class OpenPgpMessageInputStream extends InputStream { in = null; } else { try { - System.out.println("Walk " + automaton); + System.out.println("Read consume"); + consumePackets(); + } catch (PGPException e) { + throw new RuntimeException(e); + } + } + } + return r; + } + + @Override + public int read(byte[] b, int off, int len) + throws IOException { + + if (in == null) { + automaton.assertValid(); + return -1; + } + + int r = in.read(b, off, len); + if (r == -1) { + if (in instanceof OpenPgpMessageInputStream) { + in.close(); + in = null; + } else { + try { consumePackets(); } catch (PGPException e) { throw new RuntimeException(e); @@ -343,6 +371,7 @@ public class OpenPgpMessageInputStream extends InputStream { @Override public void close() throws IOException { if (closed) { + automaton.assertValid(); return; } @@ -418,15 +447,15 @@ public class OpenPgpMessageInputStream extends InputStream { public void update(byte b) { /** - for (PGPSignature prepended : prependedSignatures) { - prepended.update(b); - } - for (PGPOnePassSignature ops : onePassSignatures) { - ops.update(b); - } - for (PGPSignature detached : detachedSignatures) { - detached.update(b); - } + for (PGPSignature prepended : prependedSignatures) { + prepended.update(b); + } + for (PGPOnePassSignature ops : onePassSignatures) { + ops.update(b); + } + for (PGPSignature detached : detachedSignatures) { + detached.update(b); + } */ } From 54d7d0c7aeaac5a31dbfe15d7459074c590413dd Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Fri, 16 Sep 2022 00:51:49 +0200 Subject: [PATCH 021/763] Implement experimental signature verification (correctness only) --- .../MessageDecryptionStream.java | 278 ------------- .../OpenPgpMessageInputStream.java | 167 +++++++- .../automaton/NestingPDA.java | 339 ---------------- .../MalformedOpenPgpMessageException.java | 15 - .../OpenPgpMessageInputStreamTest.java | 368 +++++++++++++++++- .../PGPDecryptionStreamTest.java | 361 ----------------- .../automaton/NestingPDATest.java | 205 ---------- 7 files changed, 497 insertions(+), 1236 deletions(-) delete mode 100644 pgpainless-core/src/main/java/org/pgpainless/decryption_verification/MessageDecryptionStream.java delete mode 100644 pgpainless-core/src/main/java/org/pgpainless/decryption_verification/automaton/NestingPDA.java delete mode 100644 pgpainless-core/src/test/java/org/pgpainless/decryption_verification/PGPDecryptionStreamTest.java delete mode 100644 pgpainless-core/src/test/java/org/pgpainless/decryption_verification/automaton/NestingPDATest.java diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/MessageDecryptionStream.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/MessageDecryptionStream.java deleted file mode 100644 index 335e9d57..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/MessageDecryptionStream.java +++ /dev/null @@ -1,278 +0,0 @@ -// SPDX-FileCopyrightText: 2022 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.decryption_verification; - -import org.bouncycastle.bcpg.BCPGInputStream; -import org.bouncycastle.bcpg.BCPGOutputStream; -import org.bouncycastle.bcpg.ModDetectionCodePacket; -import org.bouncycastle.bcpg.OnePassSignaturePacket; -import org.bouncycastle.bcpg.Packet; -import org.bouncycastle.bcpg.PacketTags; -import org.bouncycastle.bcpg.PublicKeyEncSessionPacket; -import org.bouncycastle.bcpg.SignaturePacket; -import org.bouncycastle.bcpg.SymmetricKeyEncSessionPacket; -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.PGPOnePassSignatureList; -import org.bouncycastle.openpgp.PGPPBEEncryptedData; -import org.bouncycastle.openpgp.PGPSignatureList; -import org.bouncycastle.openpgp.operator.PBEDataDecryptorFactory; -import org.pgpainless.algorithm.OpenPgpPacket; -import org.pgpainless.decryption_verification.automaton.InputAlphabet; -import org.pgpainless.decryption_verification.automaton.NestingPDA; -import org.pgpainless.exception.MalformedOpenPgpMessageException; -import org.pgpainless.exception.MessageNotIntegrityProtectedException; -import org.pgpainless.exception.MissingDecryptionMethodException; -import org.pgpainless.implementation.ImplementationFactory; -import org.pgpainless.util.Passphrase; - -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.SequenceInputStream; -import java.util.ArrayList; -import java.util.List; -import java.util.NoSuchElementException; -import java.util.Stack; - -public class MessageDecryptionStream extends InputStream { - - private final ConsumerOptions options; - - NestingPDA automaton = new NestingPDA(); - // nested streams, outermost at the bottom of the stack - Stack packetLayers = new Stack<>(); - List pkeskList = new ArrayList<>(); - List skeskList = new ArrayList<>(); - - public MessageDecryptionStream(InputStream inputStream, ConsumerOptions options) - throws IOException, PGPException { - this.options = options; - packetLayers.push(Layer.initial(inputStream)); - walkLayer(); - } - - private void walkLayer() throws PGPException, IOException { - if (packetLayers.isEmpty()) { - return; - } - - // We are currently in the deepest layer - Layer layer = packetLayers.peek(); - BCPGInputStream inputStream = (BCPGInputStream) layer.inputStream; - - loop: while (true) { - if (inputStream.nextPacketTag() == -1) { - popLayer(); - break loop; - } - OpenPgpPacket tag = nextTagOrThrow(inputStream); - switch (tag) { - - case LIT: - automaton.next(InputAlphabet.LiteralData); - PGPLiteralData literalData = new PGPLiteralData(inputStream); - packetLayers.push(Layer.literalMessage(literalData.getDataStream())); - break loop; - - case COMP: - automaton.next(InputAlphabet.CompressedData); - PGPCompressedData compressedData = new PGPCompressedData(inputStream); - inputStream = new BCPGInputStream(compressedData.getDataStream()); - packetLayers.push(Layer.compressedData(inputStream)); - break; - - case OPS: - automaton.next(InputAlphabet.OnePassSignatures); - ByteArrayOutputStream buf = new ByteArrayOutputStream(); - BCPGOutputStream bcpgOut = new BCPGOutputStream(buf); - while (inputStream.nextPacketTag() == PacketTags.ONE_PASS_SIGNATURE || inputStream.nextPacketTag() == PacketTags.MARKER) { - Packet packet = inputStream.readPacket(); - if (packet instanceof OnePassSignaturePacket) { - OnePassSignaturePacket sig = (OnePassSignaturePacket) packet; - sig.encode(bcpgOut); - } - } - PGPOnePassSignatureList onePassSignatures = (PGPOnePassSignatureList) ImplementationFactory.getInstance() - .getPGPObjectFactory(buf.toByteArray()).nextObject(); - break; - - case SIG: - automaton.next(InputAlphabet.Signatures); - - buf = new ByteArrayOutputStream(); - bcpgOut = new BCPGOutputStream(buf); - while (inputStream.nextPacketTag() == PacketTags.SIGNATURE || inputStream.nextPacketTag() == PacketTags.MARKER) { - Packet packet = inputStream.readPacket(); - if (packet instanceof SignaturePacket) { - SignaturePacket sig = (SignaturePacket) packet; - sig.encode(bcpgOut); - } - } - PGPSignatureList signatures = (PGPSignatureList) ImplementationFactory.getInstance() - .getPGPObjectFactory(buf.toByteArray()).nextObject(); - break; - - case PKESK: - PublicKeyEncSessionPacket pkeskPacket = (PublicKeyEncSessionPacket) inputStream.readPacket(); - pkeskList.add(pkeskPacket); - break; - - case SKESK: - SymmetricKeyEncSessionPacket skeskPacket = (SymmetricKeyEncSessionPacket) inputStream.readPacket(); - skeskList.add(skeskPacket); - break; - - case SED: - if (!options.isIgnoreMDCErrors()) { - throw new MessageNotIntegrityProtectedException(); - } - // No break; we continue below! - case SEIPD: - automaton.next(InputAlphabet.EncryptedData); - PGPEncryptedDataList encryptedDataList = assembleEncryptedDataList(inputStream); - - for (PGPEncryptedData encData : encryptedDataList) { - if (encData instanceof PGPPBEEncryptedData) { - PGPPBEEncryptedData skenc = (PGPPBEEncryptedData) encData; - for (Passphrase passphrase : options.getDecryptionPassphrases()) { - PBEDataDecryptorFactory decryptorFactory = ImplementationFactory.getInstance() - .getPBEDataDecryptorFactory(passphrase); - InputStream decryptedIn = skenc.getDataStream(decryptorFactory); - packetLayers.push(Layer.encryptedData(new BCPGInputStream(decryptedIn))); - walkLayer(); - break loop; - } - } - } - throw new MissingDecryptionMethodException("Cannot decrypt message."); - - case MARKER: - inputStream.readPacket(); // discard - break; - - case SK: - case PK: - case SSK: - case PSK: - case TRUST: - case UID: - case UATTR: - throw new MalformedOpenPgpMessageException("OpenPGP packet " + tag + " MUST NOT be part of OpenPGP messages."); - case MOD: - ModDetectionCodePacket modDetectionCodePacket = (ModDetectionCodePacket) inputStream.readPacket(); - break; - case EXP_1: - case EXP_2: - case EXP_3: - case EXP_4: - throw new MalformedOpenPgpMessageException("Experimental packet " + tag + " found inside the message."); - } - } - } - - private PGPEncryptedDataList assembleEncryptedDataList(BCPGInputStream inputStream) - throws IOException { - ByteArrayOutputStream buf = new ByteArrayOutputStream(); - BCPGOutputStream bcpgOut = new BCPGOutputStream(buf); - - for (SymmetricKeyEncSessionPacket skesk : skeskList) { - bcpgOut.write(skesk.getEncoded()); - } - skeskList.clear(); - for (PublicKeyEncSessionPacket pkesk : pkeskList) { - bcpgOut.write(pkesk.getEncoded()); - } - pkeskList.clear(); - - SequenceInputStream sqin = new SequenceInputStream( - new ByteArrayInputStream(buf.toByteArray()), inputStream); - - PGPEncryptedDataList encryptedDataList = (PGPEncryptedDataList) ImplementationFactory.getInstance() - .getPGPObjectFactory(sqin).nextObject(); - return encryptedDataList; - } - - private OpenPgpPacket nextTagOrThrow(BCPGInputStream inputStream) - throws IOException, InvalidOpenPgpPacketException { - try { - return OpenPgpPacket.requireFromTag(inputStream.nextPacketTag()); - } catch (NoSuchElementException e) { - throw new InvalidOpenPgpPacketException(e.getMessage()); - } - } - - private void popLayer() throws MalformedOpenPgpMessageException { - if (packetLayers.pop().isNested) - automaton.next(InputAlphabet.EndOfSequence); - } - - @Override - public int read() throws IOException { - if (packetLayers.isEmpty()) { - automaton.assertValid(); - return -1; - } - - int r = -1; - try { - r = packetLayers.peek().inputStream.read(); - } catch (IOException e) { - } - if (r == -1) { - popLayer(); - try { - walkLayer(); - } catch (PGPException e) { - throw new RuntimeException(e); - } - return read(); - } - return r; - } - - public static class InvalidOpenPgpPacketException extends PGPException { - - public InvalidOpenPgpPacketException(String message) { - super(message); - } - } - - private static class Layer { - InputStream inputStream; - boolean isNested; - - private Layer(InputStream inputStream, boolean isNested) { - this.inputStream = inputStream; - this.isNested = isNested; - } - - static Layer initial(InputStream inputStream) { - BCPGInputStream bcpgIn; - if (inputStream instanceof BCPGInputStream) { - bcpgIn = (BCPGInputStream) inputStream; - } else { - bcpgIn = new BCPGInputStream(inputStream); - } - return new Layer(bcpgIn, true); - } - - static Layer literalMessage(InputStream inputStream) { - return new Layer(inputStream, false); - } - - static Layer compressedData(InputStream inputStream) { - return new Layer(inputStream, true); - } - - static Layer encryptedData(InputStream inputStream) { - return new Layer(inputStream, true); - } - } -} diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java index 4c6a85c8..8dff7189 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java @@ -18,11 +18,13 @@ import org.bouncycastle.openpgp.PGPPBEEncryptedData; import org.bouncycastle.openpgp.PGPPrivateKey; import org.bouncycastle.openpgp.PGPPublicKey; import org.bouncycastle.openpgp.PGPPublicKeyEncryptedData; +import org.bouncycastle.openpgp.PGPPublicKeyRing; import org.bouncycastle.openpgp.PGPSecretKey; import org.bouncycastle.openpgp.PGPSecretKeyRing; import org.bouncycastle.openpgp.PGPSignature; import org.bouncycastle.openpgp.PGPSignatureList; import org.bouncycastle.openpgp.operator.PBEDataDecryptorFactory; +import org.bouncycastle.openpgp.operator.PGPContentVerifierBuilderProvider; import org.bouncycastle.openpgp.operator.PublicKeyDataDecryptorFactory; import org.bouncycastle.openpgp.operator.SessionKeyDataDecryptorFactory; import org.pgpainless.PGPainless; @@ -38,6 +40,7 @@ import org.pgpainless.implementation.ImplementationFactory; import org.pgpainless.key.info.KeyRingInfo; import org.pgpainless.key.protection.SecretKeyRingProtector; import org.pgpainless.key.protection.UnlockSecretKey; +import org.pgpainless.signature.SignatureUtils; import org.pgpainless.util.Passphrase; import org.pgpainless.util.Tuple; @@ -57,7 +60,7 @@ public class OpenPgpMessageInputStream extends InputStream { private boolean closed = false; - private Signatures signatures = new Signatures(); + private Signatures signatures; public OpenPgpMessageInputStream(InputStream inputStream, ConsumerOptions options) throws IOException, PGPException { @@ -69,6 +72,7 @@ public class OpenPgpMessageInputStream extends InputStream { } this.options = options; + this.signatures = new Signatures(options); this.signatures.addDetachedSignatures(options.getDetachedSignatures()); consumePackets(); @@ -88,8 +92,9 @@ public class OpenPgpMessageInputStream extends InputStream { throws IOException, PGPException { System.out.println("Walk " + automaton); int tag; - loop: while ((tag = bcpgIn.nextPacketTag()) != -1) { + loop: while ((tag = getTag()) != -1) { OpenPgpPacket nextPacket = OpenPgpPacket.requireFromTag(tag); + System.out.println(nextPacket); switch (nextPacket) { // Literal Data - the literal data content is the new input stream @@ -114,9 +119,10 @@ public class OpenPgpMessageInputStream extends InputStream { // Signatures - either prepended to the message, or corresponding to the One Pass Signatures case SIG: + boolean isCorrespondingToOPS = automaton.peekStack() == StackAlphabet.ops; automaton.next(InputAlphabet.Signatures); PGPSignatureList signatureList = readSignatures(); - if (automaton.peekStack() == StackAlphabet.ops) { + if (isCorrespondingToOPS) { signatures.addOnePassCorrespondingSignatures(signatureList); } else { signatures.addPrependedSignatures(signatureList); @@ -246,6 +252,19 @@ public class OpenPgpMessageInputStream extends InputStream { } } + private int getTag() throws IOException { + try { + return bcpgIn.nextPacketTag(); + } catch (IOException e) { + if ("Stream closed".equals(e.getMessage())) { + // ZipInflater Streams sometimes close under our feet -.- + // Therefore we catch resulting IOEs and return -1 instead. + return -1; + } + throw e; + } + } + private List> findPotentialDecryptionKeys(PGPPublicKeyEncryptedData pkesk) { int algorithm = pkesk.getAlgorithm(); List> decryptionKeyCandidates = new ArrayList<>(); @@ -276,8 +295,8 @@ public class OpenPgpMessageInputStream extends InputStream { private PGPOnePassSignatureList readOnePassSignatures() throws IOException { ByteArrayOutputStream buf = new ByteArrayOutputStream(); BCPGOutputStream bcpgOut = new BCPGOutputStream(buf); - int tag = bcpgIn.nextPacketTag(); - while (tag == PacketTags.ONE_PASS_SIGNATURE || tag == PacketTags.MARKER) { + int tag; + while ((tag = getTag()) == PacketTags.ONE_PASS_SIGNATURE || tag == PacketTags.MARKER) { Packet packet = bcpgIn.readPacket(); if (tag == PacketTags.ONE_PASS_SIGNATURE) { OnePassSignaturePacket sigPacket = (OnePassSignaturePacket) packet; @@ -294,13 +313,13 @@ public class OpenPgpMessageInputStream extends InputStream { private PGPSignatureList readSignatures() throws IOException { ByteArrayOutputStream buf = new ByteArrayOutputStream(); BCPGOutputStream bcpgOut = new BCPGOutputStream(buf); - int tag = bcpgIn.nextPacketTag(); + int tag = getTag(); while (tag == PacketTags.SIGNATURE || tag == PacketTags.MARKER) { Packet packet = bcpgIn.readPacket(); if (tag == PacketTags.SIGNATURE) { SignaturePacket sigPacket = (SignaturePacket) packet; sigPacket.encode(bcpgOut); - tag = bcpgIn.nextPacketTag(); + tag = getTag(); } } bcpgOut.close(); @@ -328,6 +347,16 @@ public class OpenPgpMessageInputStream extends InputStream { byte b = (byte) r; signatures.update(b); } else { + in.close(); + in = null; + + try { + consumePackets(); + } catch (PGPException e) { + throw new RuntimeException(e); + } + signatures.finish(); + /* if (in instanceof OpenPgpMessageInputStream) { in.close(); in = null; @@ -335,10 +364,12 @@ public class OpenPgpMessageInputStream extends InputStream { try { System.out.println("Read consume"); consumePackets(); + signatures.finish(); } catch (PGPException e) { throw new RuntimeException(e); } } + */ } return r; } @@ -354,6 +385,16 @@ public class OpenPgpMessageInputStream extends InputStream { int r = in.read(b, off, len); if (r == -1) { + in.close(); + in = null; + + try { + consumePackets(); + } catch (PGPException e) { + throw new RuntimeException(e); + } + signatures.finish(); + /* if (in instanceof OpenPgpMessageInputStream) { in.close(); in = null; @@ -364,6 +405,7 @@ public class OpenPgpMessageInputStream extends InputStream { throw new RuntimeException(e); } } + */ } return r; } @@ -380,6 +422,12 @@ public class OpenPgpMessageInputStream extends InputStream { in = null; } + try { + consumePackets(); + } catch (PGPException e) { + throw new RuntimeException(e); + } + automaton.next(InputAlphabet.EndOfSequence); automaton.assertValid(); closed = true; @@ -418,50 +466,133 @@ public class OpenPgpMessageInputStream extends InputStream { } private static class Signatures { + final ConsumerOptions options; List detachedSignatures = new ArrayList<>(); List prependedSignatures = new ArrayList<>(); List onePassSignatures = new ArrayList<>(); List correspondingSignatures = new ArrayList<>(); + + private Signatures(ConsumerOptions options) { + this.options = options; + } + void addDetachedSignatures(Collection signatures) { + for (PGPSignature signature : signatures) { + long keyId = SignatureUtils.determineIssuerKeyId(signature); + PGPPublicKeyRing certificate = findCertificate(keyId); + initialize(signature, certificate, keyId); + } this.detachedSignatures.addAll(signatures); } void addPrependedSignatures(PGPSignatureList signatures) { + System.out.println("Adding " + signatures.size() + " prepended Signatures"); for (PGPSignature signature : signatures) { + long keyId = SignatureUtils.determineIssuerKeyId(signature); + PGPPublicKeyRing certificate = findCertificate(keyId); + initialize(signature, certificate, keyId); this.prependedSignatures.add(signature); } } void addOnePassSignatures(PGPOnePassSignatureList signatures) { + System.out.println("Adding " + signatures.size() + " OPSs"); for (PGPOnePassSignature ops : signatures) { + PGPPublicKeyRing certificate = findCertificate(ops.getKeyID()); + initialize(ops, certificate); this.onePassSignatures.add(ops); } } void addOnePassCorrespondingSignatures(PGPSignatureList signatures) { + System.out.println("Adding " + signatures.size() + " Corresponding Signatures"); for (PGPSignature signature : signatures) { correspondingSignatures.add(signature); } } + private void initialize(PGPSignature signature, PGPPublicKeyRing certificate, long keyId) { + if (certificate == null) { + // SHIT + return; + } + PGPContentVerifierBuilderProvider verifierProvider = ImplementationFactory.getInstance() + .getPGPContentVerifierBuilderProvider(); + try { + signature.init(verifierProvider, certificate.getPublicKey(keyId)); + } catch (PGPException e) { + throw new RuntimeException(e); + } + } + + private void initialize(PGPOnePassSignature ops, PGPPublicKeyRing certificate) { + if (certificate == null) { + return; + } + PGPContentVerifierBuilderProvider verifierProvider = ImplementationFactory.getInstance() + .getPGPContentVerifierBuilderProvider(); + try { + ops.init(verifierProvider, certificate.getPublicKey(ops.getKeyID())); + } catch (PGPException e) { + throw new RuntimeException(e); + } + } + + private PGPPublicKeyRing findCertificate(long keyId) { + for (PGPPublicKeyRing cert : options.getCertificates()) { + PGPPublicKey verificationKey = cert.getPublicKey(keyId); + if (verificationKey != null) { + return cert; + } + } + return null; // TODO: Missing cert for sig + } + public void update(byte b) { - /** - for (PGPSignature prepended : prependedSignatures) { - prepended.update(b); - } - for (PGPOnePassSignature ops : onePassSignatures) { - ops.update(b); - } - for (PGPSignature detached : detachedSignatures) { - detached.update(b); - } - */ + for (PGPSignature prepended : prependedSignatures) { + prepended.update(b); + } + for (PGPOnePassSignature ops : onePassSignatures) { + ops.update(b); + } + for (PGPSignature detached : detachedSignatures) { + detached.update(b); + } } public void finish() { for (PGPSignature detached : detachedSignatures) { + boolean verified = false; + try { + verified = detached.verify(); + } catch (PGPException e) { + System.out.println(e.getMessage()); + } + System.out.println("Detached Signature by " + Long.toHexString(detached.getKeyID()) + " is " + (verified ? "verified" : "unverified")); + } + for (PGPSignature prepended : prependedSignatures) { + boolean verified = false; + try { + verified = prepended.verify(); + } catch (PGPException e) { + System.out.println(e.getMessage()); + } + System.out.println("Prepended Signature by " + Long.toHexString(prepended.getKeyID()) + " is " + (verified ? "verified" : "unverified")); + } + + + for (int i = 0; i < onePassSignatures.size(); i++) { + PGPOnePassSignature ops = onePassSignatures.get(i); + PGPSignature signature = correspondingSignatures.get(correspondingSignatures.size() - i - 1); + boolean verified = false; + try { + verified = ops.verify(signature); + } catch (PGPException e) { + System.out.println(e.getMessage()); + } + System.out.println("One-Pass-Signature by " + Long.toHexString(ops.getKeyID()) + " is " + (verified ? "verified" : "unverified")); } } } diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/automaton/NestingPDA.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/automaton/NestingPDA.java deleted file mode 100644 index cf5ee674..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/automaton/NestingPDA.java +++ /dev/null @@ -1,339 +0,0 @@ -package org.pgpainless.decryption_verification.automaton; - -import org.pgpainless.exception.MalformedOpenPgpMessageException; - -import java.util.Stack; - -import static org.pgpainless.decryption_verification.automaton.StackAlphabet.msg; -import static org.pgpainless.decryption_verification.automaton.StackAlphabet.ops; -import static org.pgpainless.decryption_verification.automaton.StackAlphabet.terminus; - -/** - * Pushdown Automaton to verify the correct syntax of OpenPGP messages during decryption. - *

- * OpenPGP messages MUST follow certain rules in order to be well-formed. - * Section §11.3. of RFC4880 specifies a formal grammar for OpenPGP messages. - *

- * This grammar was transformed into a pushdown automaton, which is implemented below. - * The automaton only ends up in a valid state ({@link #isValid()} iff the OpenPGP message conformed to the - * grammar. - *

- * There are some specialties with this implementation though: - * Bouncy Castle combines ESKs and Encrypted Data Packets into a single object, so we do not have to - * handle those manually. - *

- * Bouncy Castle further combines OnePassSignatures and Signatures into lists, so instead of pushing multiple - * 'o's onto the stack repeatedly, a sequence of OnePassSignatures causes a single 'o' to be pushed to the stack. - * The same is true for Signatures. - *

- * Therefore, a message is valid, even if the number of OnePassSignatures and Signatures does not match. - * If a message contains at least one OnePassSignature, it is sufficient if there is at least one Signature to - * not cause a {@link MalformedOpenPgpMessageException}. - * - * @see RFC4880 §11.3. OpenPGP Messages - */ -public class NestingPDA { - - /** - * Set of states of the automaton. - * Each state defines its valid transitions in their {@link State#transition(InputAlphabet, NestingPDA)} - * method. - */ - public enum State { - - OpenPgpMessage { - @Override - State transition(InputAlphabet input, NestingPDA automaton) throws MalformedOpenPgpMessageException { - StackAlphabet stackItem = automaton.popStack(); - if (stackItem != msg) { - throw new MalformedOpenPgpMessageException(this, input, stackItem); - } - switch (input) { - - case LiteralData: - return LiteralMessage; - - case Signatures: - automaton.pushStack(msg); - return OpenPgpMessage; - - case OnePassSignatures: - automaton.pushStack(ops); - automaton.pushStack(msg); - return OpenPgpMessage; - - case CompressedData: - return CompressedMessage; - - case EncryptedData: - return EncryptedMessage; - - case EndOfSequence: - default: - throw new MalformedOpenPgpMessageException(this, input, stackItem); - } - } - }, - - LiteralMessage { - @Override - State transition(InputAlphabet input, NestingPDA automaton) throws MalformedOpenPgpMessageException { - StackAlphabet stackItem = automaton.popStack(); - switch (input) { - - case Signatures: - if (stackItem == ops) { - return CorrespondingSignature; - } else { - throw new MalformedOpenPgpMessageException(this, input, stackItem); - } - - case EndOfSequence: - if (stackItem == terminus && automaton.stack.isEmpty()) { - return Valid; - } else { - throw new MalformedOpenPgpMessageException(this, input, stackItem); - } - - case LiteralData: - case OnePassSignatures: - case CompressedData: - case EncryptedData: - default: - throw new MalformedOpenPgpMessageException(this, input, stackItem); - } - } - }, - - CompressedMessage { - @Override - State transition(InputAlphabet input, NestingPDA automaton) throws MalformedOpenPgpMessageException { - StackAlphabet stackItem = automaton.popStack(); - switch (input) { - case Signatures: - if (stackItem == ops) { - return CorrespondingSignature; - } else { - throw new MalformedOpenPgpMessageException(this, input, stackItem); - } - - case EndOfSequence: - if (stackItem == terminus && automaton.stack.isEmpty()) { - return Valid; - } else { - throw new MalformedOpenPgpMessageException(this, input, stackItem); - } - - case LiteralData: - case OnePassSignatures: - case CompressedData: - case EncryptedData: - default: - throw new MalformedOpenPgpMessageException(this, input, stackItem); - } - } - }, - - EncryptedMessage { - @Override - State transition(InputAlphabet input, NestingPDA automaton) throws MalformedOpenPgpMessageException { - StackAlphabet stackItem = automaton.popStack(); - switch (input) { - case Signatures: - if (stackItem == ops) { - return CorrespondingSignature; - } else { - throw new MalformedOpenPgpMessageException(this, input, stackItem); - } - - case EndOfSequence: - if (stackItem == terminus && automaton.stack.isEmpty()) { - return Valid; - } else { - throw new MalformedOpenPgpMessageException(this, input, stackItem); - } - - case LiteralData: - case OnePassSignatures: - case CompressedData: - case EncryptedData: - default: - throw new MalformedOpenPgpMessageException(this, input, stackItem); - } - } - }, - - CorrespondingSignature { - @Override - State transition(InputAlphabet input, NestingPDA automaton) throws MalformedOpenPgpMessageException { - StackAlphabet stackItem = automaton.popStack(); - if (stackItem == terminus && input == InputAlphabet.EndOfSequence && automaton.stack.isEmpty()) { - return Valid; - } else { - throw new MalformedOpenPgpMessageException(this, input, stackItem); - } - } - }, - - Valid { - @Override - State transition(InputAlphabet input, NestingPDA automaton) throws MalformedOpenPgpMessageException { - throw new MalformedOpenPgpMessageException(this, input, null); - } - }, - ; - - /** - * Pop the automatons stack and transition to another state. - * If no valid transition from the current state is available given the popped stack item and input symbol, - * a {@link MalformedOpenPgpMessageException} is thrown. - * Otherwise, the stack is manipulated according to the valid transition and the new state is returned. - * - * @param input input symbol - * @param automaton automaton - * @return new state of the automaton - * @throws MalformedOpenPgpMessageException in case of an illegal input symbol - */ - abstract State transition(InputAlphabet input, NestingPDA automaton) throws MalformedOpenPgpMessageException; - } - - private final Stack stack = new Stack<>(); - private State state; - // Some OpenPGP packets have nested contents (e.g. compressed / encrypted data). - NestingPDA nestedSequence = null; - - public NestingPDA() { - state = State.OpenPgpMessage; - stack.push(terminus); - stack.push(msg); - } - - /** - * Process the next input packet. - * - * @param input input - * @throws MalformedOpenPgpMessageException in case the input packet is illegal here - */ - public void next(InputAlphabet input) throws MalformedOpenPgpMessageException { - _next(input); - } - - /** - * Process the next input packet. - * This method returns true, iff the given input triggered a successful closing of this PDAs nested PDA. - *

- * This is for example the case, if the current packet is a Compressed Data packet which contains a - * valid nested OpenPGP message and the last input was {@link InputAlphabet#EndOfSequence} indicating the - * end of the Compressed Data packet. - *

- * If the input triggered this PDAs nested PDA to close its nested PDA, this method returns false - * in order to prevent this PDA from closing its nested PDA prematurely. - * - * @param input input - * @return true if this just closed its nested sequence, false otherwise - * @throws MalformedOpenPgpMessageException if the input is illegal - */ - private boolean _next(InputAlphabet input) throws MalformedOpenPgpMessageException { - if (nestedSequence != null) { - boolean sequenceInNestedSequenceWasClosed = nestedSequence._next(input); - if (sequenceInNestedSequenceWasClosed) return false; // No need to close out nested sequence too. - } else { - // make a state transition in this automaton - state = state.transition(input, this); - - // If the processed packet contains nested sequence, open nested automaton for it - if (input == InputAlphabet.CompressedData || input == InputAlphabet.EncryptedData) { - nestedSequence = new NestingPDA(); - } - } - - if (input != InputAlphabet.EndOfSequence) { - return false; - } - - // Close nested sequence if needed - boolean nestedIsInnerMost = nestedSequence != null && nestedSequence.isInnerMost(); - if (nestedIsInnerMost) { - if (nestedSequence.isValid()) { - // Close nested sequence - nestedSequence = null; - return true; - } else { - throw new MalformedOpenPgpMessageException("Climbing up nested message validation failed." + - " Automaton for current nesting level is not in valid state: " + nestedSequence.getState() + " " + nestedSequence.stack.peek() + " (Input was " + input + ")"); - } - } - return false; - } - - /** - * Return the current state of the PDA. - * - * @return state - */ - private State getState() { - return state; - } - - /** - * Return true, if the PDA is in a valid state (the OpenPGP message is valid). - * - * @return true if valid, false otherwise - */ - public boolean isValid() { - return getState() == State.Valid && stack.isEmpty(); - } - - public void assertValid() throws MalformedOpenPgpMessageException { - if (!isValid()) { - throw new MalformedOpenPgpMessageException("Pushdown Automaton is not in an acceptable state: " + toString()); - } - } - - /** - * Pop an item from the stack. - * - * @return stack item - */ - private StackAlphabet popStack() { - return stack.pop(); - } - - /** - * Push an item onto the stack. - * - * @param item item - */ - private void pushStack(StackAlphabet item) { - stack.push(item); - } - - /** - * Return true, if this packet sequence has no nested sequence. - * A nested sequence is for example the content of a Compressed Data packet. - * - * @return true if PDA is innermost, false if it has a nested sequence - */ - private boolean isInnerMost() { - return nestedSequence == null; - } - - @Override - public String toString() { - StringBuilder out = new StringBuilder("State: ").append(state) - .append(", Stack (asc.): ").append(stack) - .append('\n'); - if (nestedSequence != null) { - // recursively call toString() on nested PDAs and indent their representation - String nestedToString = nestedSequence.toString(); - String[] lines = nestedToString.split("\n"); - for (int i = 0; i < lines.length; i++) { - String nestedLine = lines[i]; - out.append(i == 0 ? "⤷ " : " ") // indent nested PDA - .append(nestedLine) - .append('\n'); - } - } - return out.toString(); - } -} diff --git a/pgpainless-core/src/main/java/org/pgpainless/exception/MalformedOpenPgpMessageException.java b/pgpainless-core/src/main/java/org/pgpainless/exception/MalformedOpenPgpMessageException.java index 21b6e807..0069209c 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/exception/MalformedOpenPgpMessageException.java +++ b/pgpainless-core/src/main/java/org/pgpainless/exception/MalformedOpenPgpMessageException.java @@ -5,7 +5,6 @@ package org.pgpainless.exception; import org.pgpainless.decryption_verification.automaton.InputAlphabet; -import org.pgpainless.decryption_verification.automaton.NestingPDA; import org.pgpainless.decryption_verification.automaton.PDA; import org.pgpainless.decryption_verification.automaton.StackAlphabet; @@ -21,21 +20,7 @@ public class MalformedOpenPgpMessageException extends RuntimeException { super(message); } - public MalformedOpenPgpMessageException(String message, MalformedOpenPgpMessageException cause) { - super(message, cause); - } - - public MalformedOpenPgpMessageException(NestingPDA.State state, - InputAlphabet input, - StackAlphabet stackItem) { - this("Invalid input: There is no legal transition from state '" + state + "' for input '" + input + "' when '" + stackItem + "' is on top of the stack."); - } - public MalformedOpenPgpMessageException(PDA.State state, InputAlphabet input, StackAlphabet stackItem) { this("Invalid input: There is no legal transition from state '" + state + "' for input '" + input + "' when '" + stackItem + "' is on top of the stack."); } - - public MalformedOpenPgpMessageException(String message, PDA automaton) { - super(message + automaton.toString()); - } } diff --git a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStreamTest.java b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStreamTest.java index 5b42101c..af1fac44 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStreamTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStreamTest.java @@ -1,87 +1,402 @@ package org.pgpainless.decryption_verification; import org.bouncycastle.bcpg.ArmoredInputStream; +import org.bouncycastle.bcpg.ArmoredOutputStream; +import org.bouncycastle.bcpg.CompressionAlgorithmTags; +import org.bouncycastle.openpgp.PGPCompressedDataGenerator; import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.PGPLiteralData; +import org.bouncycastle.openpgp.PGPLiteralDataGenerator; +import org.bouncycastle.openpgp.PGPPublicKeyRing; import org.bouncycastle.openpgp.PGPSecretKeyRing; +import org.bouncycastle.openpgp.PGPSignature; import org.bouncycastle.util.io.Streams; import org.junit.jupiter.api.Test; import org.pgpainless.PGPainless; +import org.pgpainless.algorithm.CompressionAlgorithm; +import org.pgpainless.encryption_signing.EncryptionOptions; +import org.pgpainless.encryption_signing.EncryptionResult; +import org.pgpainless.encryption_signing.EncryptionStream; +import org.pgpainless.encryption_signing.ProducerOptions; +import org.pgpainless.encryption_signing.SigningOptions; import org.pgpainless.exception.MalformedOpenPgpMessageException; +import org.pgpainless.key.protection.SecretKeyRingProtector; import org.pgpainless.util.ArmoredInputStreamFactory; import org.pgpainless.util.Passphrase; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; +import java.io.OutputStream; import java.nio.charset.StandardCharsets; +import java.security.InvalidAlgorithmParameterException; +import java.security.NoSuchAlgorithmException; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.pgpainless.decryption_verification.PGPDecryptionStreamTest.COMP; -import static org.pgpainless.decryption_verification.PGPDecryptionStreamTest.COMP_COMP_LIT; -import static org.pgpainless.decryption_verification.PGPDecryptionStreamTest.COMP_LIT; -import static org.pgpainless.decryption_verification.PGPDecryptionStreamTest.KEY; -import static org.pgpainless.decryption_verification.PGPDecryptionStreamTest.LIT; -import static org.pgpainless.decryption_verification.PGPDecryptionStreamTest.LIT_LIT; -import static org.pgpainless.decryption_verification.PGPDecryptionStreamTest.PASSPHRASE; -import static org.pgpainless.decryption_verification.PGPDecryptionStreamTest.PENC_COMP_LIT; -import static org.pgpainless.decryption_verification.PGPDecryptionStreamTest.PLAINTEXT; -import static org.pgpainless.decryption_verification.PGPDecryptionStreamTest.SENC_LIT; -import static org.pgpainless.decryption_verification.PGPDecryptionStreamTest.SIG_LIT; public class OpenPgpMessageInputStreamTest { + + public static final String KEY = "-----BEGIN PGP PRIVATE KEY BLOCK-----\n" + + "Version: PGPainless\n" + + "Comment: DA05 848F 37D4 68E6 F982 C889 7A70 1FC6 904D 3F4C\n" + + "Comment: Alice \n" + + "\n" + + "lFgEYxzSCBYJKwYBBAHaRw8BAQdAeJU8m4GOJb1eQgv/ryilFHRfNLTYFMNqL6zj\n" + + "r0vF7dsAAP42rAtngpJ6dZxoZlJX0Je65zk1VMPeTrXaWfPS2HSKBRGptBxBbGlj\n" + + "ZSA8YWxpY2VAcGdwYWlubGVzcy5vcmc+iI8EExYKAEEFAmMc0ggJEHpwH8aQTT9M\n" + + "FiEE2gWEjzfUaOb5gsiJenAfxpBNP0wCngECmwEFFgIDAQAECwkIBwUVCgkICwKZ\n" + + "AQAApZEBALUXHtvswPZG28YO+16Men6/fpk+scvqpNMnD4ty3IkAAPwK6TuXjNnZ\n" + + "0XuWdnilvLMV23Ai1d5g6em+lwLK5M2SApxdBGMc0ggSCisGAQQBl1UBBQEBB0D8\n" + + "mNUVX8y2MXFaSeFYqOTPFnGT7dgNVdn6yc0UtkkHOgMBCAcAAP9y9OtP4SX9voPb\n" + + "ID2u9PkJKgo4hTB8NK5LouGppdRtEBGriHUEGBYKAB0FAmMc0ggCngECmwwFFgID\n" + + "AQAECwkIBwUVCgkICwAKCRB6cB/GkE0/TAywAQDpZRJS/joFH4+xcwheqWfI7ay/\n" + + "WfojUoGQMYGnUjsgYwEAkceRUsgkqI0SVgYvuglfaQpZ9k2ns1mZGVLkXvu/yQyc\n" + + "WARjHNIIFgkrBgEEAdpHDwEBB0BGN9BybSOrj8B6gim1SjbB/IiqAshlzMDunVkQ\n" + + "X23npQABAJqvjOOY7qhBuTusC5/Q5+25iLrhMn4TI+LXlJHMVNOaE0OI1QQYFgoA\n" + + "fQUCYxzSCAKeAQKbAgUWAgMBAAQLCQgHBRUKCQgLXyAEGRYKAAYFAmMc0ggACgkQ\n" + + "KALh4BJQXl6yTQD/dh0N5228Uwtu7XHy6dmpMRX62cac5tXQ9WaDzpy8STgBAMdn\n" + + "Mq948UOYEhdk/ZY2/hwux/4t+FHvqrXW8ziBe4cLAAoJEHpwH8aQTT9M1hQA/3Ms\n" + + "P3kzoed3VsWu1ZMr7dKEngbc6SoJ2XPayzN0QYJaAQCIY5NcT9mZF97HWV3Vgeum\n" + + "00sWMHXfkW3+nl5OpUZaDA==\n" + + "=THgv\n" + + "-----END PGP PRIVATE KEY BLOCK-----"; + + public static final String PLAINTEXT = "Hello, World!\n"; + public static final String PASSPHRASE = "sw0rdf1sh"; + + public static final String LIT = "" + + "-----BEGIN PGP MESSAGE-----\n" + + "Version: PGPainless\n" + + "\n" + + "yxRiAAAAAABIZWxsbywgV29ybGQhCg==\n" + + "=WGju\n" + + "-----END PGP MESSAGE-----"; + + public static final String LIT_LIT = "" + + "-----BEGIN PGP MESSAGE-----\n" + + "Version: BCPG v1.71\n" + + "\n" + + "yxRiAAAAAABIZWxsbywgV29ybGQhCssUYgAAAAAASGVsbG8sIFdvcmxkIQo=\n" + + "=A91Q\n" + + "-----END PGP MESSAGE-----"; + + public static final String COMP_LIT = "" + + "-----BEGIN PGP MESSAGE-----\n" + + "Version: BCPG v1.71\n" + + "\n" + + "owE7LZLEAAIeqTk5+ToK4flFOSmKXAA=\n" + + "=ZYDg\n" + + "-----END PGP MESSAGE-----"; + + public static final String COMP = "" + + "-----BEGIN PGP MESSAGE-----\n" + + "Version: BCPG v1.71\n" + + "\n" + + "owEDAA==\n" + + "=MDzg\n" + + "-----END PGP MESSAGE-----"; + + public static final String COMP_COMP_LIT = "" + + "-----BEGIN PGP MESSAGE-----\n" + + "Version: BCPG v1.71\n" + + "\n" + + "owEBRwC4/6MDQlpoOTFBWSZTWVuW2KAAAAr3hGAQBABgBABAAIAWBJAAAAggADFM\n" + + "ABNBqBo00N6puqWR+TqInoXQ58XckU4UJBbltigA\n" + + "=K9Zl\n" + + "-----END PGP MESSAGE-----"; + + public static final String SIG_LIT = "" + + "-----BEGIN PGP MESSAGE-----\n" + + "Version: BCPG v1.71\n" + + "\n" + + "iHUEABYKACcFAmMc1i0JECgC4eASUF5eFiEEjN3RiJxCf/TyYOQjKALh4BJQXl4A\n" + + "AHkrAP98uPpqrgIix7epgL7MM1cjXXGSxqbDfXHwgptk1YGQlgD/fw89VGcXwFaI\n" + + "2k7kpXQYy/1BqnovM/jZ3X3mXhhTaAOjATstksQAAh6pOTn5Ogrh+UU5KYpcAA==\n" + + "=WKPn\n" + + "-----END PGP MESSAGE-----"; + + public static final String SENC_LIT = "" + + "-----BEGIN PGP MESSAGE-----\n" + + "Version: PGPainless\n" + + "\n" + + "jA0ECQMCuZ0qHNXWnGhg0j8Bdm1cxV65sYb7jDgb4rRMtdNpQ1dC4UpSYuk9YWS2\n" + + "DpNEijbX8b/P1UOK2kJczNDADMRegZuLEI+dNsBnJjk=\n" + + "=i4Y0\n" + + "-----END PGP MESSAGE-----"; + + public static final String PENC_COMP_LIT = "" + + "-----BEGIN PGP MESSAGE-----\n" + + "Version: PGPainless\n" + + "\n" + + "hF4Dyqa/GWUy6WsSAQdAQ62BwmUt8Iby0+jvrLhMgST79KR/as+dyl0nf1uki2sw\n" + + "Thg1Ojtf0hOyJgcpQ4nP2Q0wYFR0F1sCydaIlTGreYZHlGtybP7/Ml6KNZILTRWP\n" + + "0kYBkGBgK7oQWRIVyoF2POvEP6EX1X8nvQk7O3NysVdRVbnia7gE3AzRYuha4kxs\n" + + "pI6xJkntLMS3K6him1Y9FHINIASFSB+C\n" + + "=5p00\n" + + "-----END PGP MESSAGE-----"; + + public static final String OPS_LIT_SIG = "" + + "-----BEGIN PGP MESSAGE-----\n" + + "Version: PGPainless\n" + + "\n" + + "kA0DAAoWKALh4BJQXl4ByxRiAAAAAABIZWxsbywgV29ybGQhCoh1BAAWCgAnBQJj\n" + + "I3fSCRAoAuHgElBeXhYhBIzd0YicQn/08mDkIygC4eASUF5eAADLOgEA766VyMMv\n" + + "sxfQwQHly3T6ySHSNhYEpoyvdxVqhjBBR+EA/3i6C8lKFPPTh/PvTGbVFOl+eUSV\n" + + "I0w3c+BRY/pO0m4H\n" + + "=tkTV\n" + + "-----END PGP MESSAGE-----"; + + public static void main(String[] args) throws Exception { + // genLIT(); + // genLIT_LIT(); + // genCOMP_LIT(); + // genCOMP(); + // genCOMP_COMP_LIT(); + // genKey(); + // genSIG_LIT(); + // genSENC_LIT(); + // genPENC_COMP_LIT(); + genOPS_LIT_SIG(); + } + + public static void genLIT() throws IOException { + ArmoredOutputStream armorOut = new ArmoredOutputStream(System.out); + PGPLiteralDataGenerator litGen = new PGPLiteralDataGenerator(); + OutputStream litOut = litGen.open(armorOut, PGPLiteralDataGenerator.BINARY, "", PGPLiteralData.NOW, new byte[1 << 9]); + litOut.write(PLAINTEXT.getBytes(StandardCharsets.UTF_8)); + litOut.close(); + armorOut.close(); + } + + public static void genLIT_LIT() throws IOException { + ArmoredOutputStream armorOut = new ArmoredOutputStream(System.out); + PGPLiteralDataGenerator litGen = new PGPLiteralDataGenerator(); + OutputStream litOut = litGen.open(armorOut, PGPLiteralDataGenerator.BINARY, "", PGPLiteralData.NOW, new byte[1 << 9]); + litOut.write(PLAINTEXT.getBytes(StandardCharsets.UTF_8)); + litOut.close(); + + litOut = litGen.open(armorOut, PGPLiteralDataGenerator.BINARY, "", PGPLiteralData.NOW, new byte[1 << 9]); + litOut.write(PLAINTEXT.getBytes(StandardCharsets.UTF_8)); + litOut.close(); + + armorOut.close(); + } + + public static void genCOMP_LIT() throws IOException { + ArmoredOutputStream armorOut = new ArmoredOutputStream(System.out); + PGPCompressedDataGenerator compGen = new PGPCompressedDataGenerator(CompressionAlgorithmTags.ZIP); + OutputStream compOut = compGen.open(armorOut); + PGPLiteralDataGenerator litGen = new PGPLiteralDataGenerator(); + OutputStream litOut = litGen.open(compOut, PGPLiteralDataGenerator.BINARY, "", PGPLiteralData.NOW, new byte[1 << 9]); + litOut.write(PLAINTEXT.getBytes(StandardCharsets.UTF_8)); + litOut.close(); + compOut.close(); + armorOut.close(); + } + + public static void genCOMP() throws IOException { + ArmoredOutputStream armorOut = new ArmoredOutputStream(System.out); + PGPCompressedDataGenerator compGen = new PGPCompressedDataGenerator(CompressionAlgorithmTags.ZIP); + OutputStream compOut = compGen.open(armorOut); + compOut.close(); + armorOut.close(); + } + + public static void genCOMP_COMP_LIT() throws IOException { + ArmoredOutputStream armorOut = new ArmoredOutputStream(System.out); + + PGPCompressedDataGenerator compGen1 = new PGPCompressedDataGenerator(CompressionAlgorithmTags.ZIP); + OutputStream compOut1 = compGen1.open(armorOut); + + PGPCompressedDataGenerator compGen2 = new PGPCompressedDataGenerator(CompressionAlgorithmTags.BZIP2); + OutputStream compOut2 = compGen2.open(compOut1); + + PGPLiteralDataGenerator litGen = new PGPLiteralDataGenerator(); + OutputStream litOut = litGen.open(compOut2, PGPLiteralDataGenerator.BINARY, "", PGPLiteralDataGenerator.NOW, new byte[1 << 9]); + + litOut.write(PLAINTEXT.getBytes(StandardCharsets.UTF_8)); + litOut.close(); + compOut2.close(); + compOut1.close(); + armorOut.close(); + } + + public static void genKey() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, IOException { + System.out.println(PGPainless.asciiArmor( + PGPainless.generateKeyRing().modernKeyRing("Alice ") + )); + } + + public static void genSIG_LIT() throws PGPException, IOException { + PGPSecretKeyRing secretKeys = PGPainless.readKeyRing().secretKeyRing(KEY); + ByteArrayOutputStream msgOut = new ByteArrayOutputStream(); + EncryptionStream signer = PGPainless.encryptAndOrSign() + .onOutputStream(msgOut) + .withOptions( + ProducerOptions.sign( + SigningOptions.get() + .addDetachedSignature(SecretKeyRingProtector.unprotectedKeys(), secretKeys) + ).setAsciiArmor(false) + ); + + Streams.pipeAll(new ByteArrayInputStream(PLAINTEXT.getBytes(StandardCharsets.UTF_8)), signer); + signer.close(); + EncryptionResult result = signer.getResult(); + PGPSignature detachedSignature = result.getDetachedSignatures().get(result.getDetachedSignatures().keySet().iterator().next()).iterator().next(); + + ByteArrayOutputStream out = new ByteArrayOutputStream(); + ArmoredOutputStream armorOut = new ArmoredOutputStream(out); + armorOut.flush(); + detachedSignature.encode(armorOut); + armorOut.write(msgOut.toByteArray()); + armorOut.close(); + + String armored = out.toString(); + System.out.println(armored + .replace("-----BEGIN PGP SIGNATURE-----\n", "-----BEGIN PGP MESSAGE-----\n") + .replace("-----END PGP SIGNATURE-----", "-----END PGP MESSAGE-----")); + } + + public static void genSENC_LIT() throws PGPException, IOException { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + EncryptionStream enc = PGPainless.encryptAndOrSign() + .onOutputStream(out) + .withOptions(ProducerOptions.encrypt(EncryptionOptions.get() + .addPassphrase(Passphrase.fromPassword(PASSPHRASE))) + .overrideCompressionAlgorithm(CompressionAlgorithm.UNCOMPRESSED)); + enc.write(PLAINTEXT.getBytes(StandardCharsets.UTF_8)); + enc.close(); + + System.out.println(out); + } + + public static void genPENC_COMP_LIT() throws IOException, PGPException { + PGPSecretKeyRing secretKeys = PGPainless.readKeyRing().secretKeyRing(KEY); + PGPPublicKeyRing cert = PGPainless.extractCertificate(secretKeys); + + ByteArrayOutputStream out = new ByteArrayOutputStream(); + EncryptionStream enc = PGPainless.encryptAndOrSign() + .onOutputStream(out) + .withOptions(ProducerOptions.encrypt(EncryptionOptions.get() + .addRecipient(cert)) + .overrideCompressionAlgorithm(CompressionAlgorithm.ZLIB)); + + Streams.pipeAll(new ByteArrayInputStream(PLAINTEXT.getBytes(StandardCharsets.UTF_8)), enc); + enc.close(); + + System.out.println(out); + } + + public static void genOPS_LIT_SIG() throws PGPException, IOException { + PGPSecretKeyRing secretKeys = PGPainless.readKeyRing().secretKeyRing(KEY); + + ByteArrayOutputStream out = new ByteArrayOutputStream(); + EncryptionStream enc = PGPainless.encryptAndOrSign() + .onOutputStream(out) + .withOptions(ProducerOptions.sign(SigningOptions.get() + .addSignature(SecretKeyRingProtector.unprotectedKeys(), secretKeys)) + .overrideCompressionAlgorithm(CompressionAlgorithm.UNCOMPRESSED)); + Streams.pipeAll(new ByteArrayInputStream(PLAINTEXT.getBytes(StandardCharsets.UTF_8)), enc); + enc.close(); + + System.out.println(out); + } + @Test public void testProcessLIT() throws IOException, PGPException { - String plain = process(LIT, ConsumerOptions.get()); + String plain = processReadBuffered(LIT, ConsumerOptions.get()); + assertEquals(PLAINTEXT, plain); + + plain = processReadSequential(LIT, ConsumerOptions.get()); assertEquals(PLAINTEXT, plain); } @Test public void testProcessLIT_LIT_fails() { assertThrows(MalformedOpenPgpMessageException.class, - () -> process(LIT_LIT, ConsumerOptions.get())); + () -> processReadBuffered(LIT_LIT, ConsumerOptions.get())); + + assertThrows(MalformedOpenPgpMessageException.class, + () -> processReadSequential(LIT_LIT, ConsumerOptions.get())); } @Test public void testProcessCOMP_LIT() throws PGPException, IOException { - String plain = process(COMP_LIT, ConsumerOptions.get()); + String plain = processReadBuffered(COMP_LIT, ConsumerOptions.get()); + assertEquals(PLAINTEXT, plain); + + plain = processReadSequential(COMP_LIT, ConsumerOptions.get()); assertEquals(PLAINTEXT, plain); } @Test public void testProcessCOMP_fails() { assertThrows(MalformedOpenPgpMessageException.class, - () -> process(COMP, ConsumerOptions.get())); + () -> processReadBuffered(COMP, ConsumerOptions.get())); + + assertThrows(MalformedOpenPgpMessageException.class, + () -> processReadSequential(COMP, ConsumerOptions.get())); } @Test public void testProcessCOMP_COMP_LIT() throws PGPException, IOException { - String plain = process(COMP_COMP_LIT, ConsumerOptions.get()); + String plain = processReadBuffered(COMP_COMP_LIT, ConsumerOptions.get()); + assertEquals(PLAINTEXT, plain); + + plain = processReadSequential(COMP_COMP_LIT, ConsumerOptions.get()); assertEquals(PLAINTEXT, plain); } @Test public void testProcessSIG_LIT() throws PGPException, IOException { - String plain = process(SIG_LIT, ConsumerOptions.get()); + PGPPublicKeyRing cert = PGPainless.extractCertificate( + PGPainless.readKeyRing().secretKeyRing(KEY)); + + String plain = processReadBuffered(SIG_LIT, ConsumerOptions.get() + .addVerificationCert(cert)); + assertEquals(PLAINTEXT, plain); + + plain = processReadSequential(SIG_LIT, ConsumerOptions.get() + .addVerificationCert(cert)); assertEquals(PLAINTEXT, plain); } @Test public void testProcessSENC_LIT() throws PGPException, IOException { - String plain = process(SENC_LIT, ConsumerOptions.get().addDecryptionPassphrase(Passphrase.fromPassword(PASSPHRASE))); + String plain = processReadBuffered(SENC_LIT, ConsumerOptions.get().addDecryptionPassphrase(Passphrase.fromPassword(PASSPHRASE))); + assertEquals(PLAINTEXT, plain); + + plain = processReadSequential(SENC_LIT, ConsumerOptions.get().addDecryptionPassphrase(Passphrase.fromPassword(PASSPHRASE))); assertEquals(PLAINTEXT, plain); } @Test public void testProcessPENC_COMP_LIT() throws IOException, PGPException { PGPSecretKeyRing secretKeys = PGPainless.readKeyRing().secretKeyRing(KEY); - String plain = process(PENC_COMP_LIT, ConsumerOptions.get() + String plain = processReadBuffered(PENC_COMP_LIT, ConsumerOptions.get() + .addDecryptionKey(secretKeys)); + assertEquals(PLAINTEXT, plain); + + plain = processReadSequential(PENC_COMP_LIT, ConsumerOptions.get() .addDecryptionKey(secretKeys)); assertEquals(PLAINTEXT, plain); } - private String process(String armoredMessage, ConsumerOptions options) throws PGPException, IOException { + @Test + public void testProcessOPS_LIT_SIG() throws IOException, PGPException { + PGPPublicKeyRing cert = PGPainless.extractCertificate(PGPainless.readKeyRing().secretKeyRing(KEY)); + String plain = processReadBuffered(OPS_LIT_SIG, ConsumerOptions.get() + .addVerificationCert(cert)); + assertEquals(PLAINTEXT, plain); + + plain = processReadSequential(OPS_LIT_SIG, ConsumerOptions.get() + .addVerificationCert(cert)); + assertEquals(PLAINTEXT, plain); + } + + private String processReadBuffered(String armoredMessage, ConsumerOptions options) throws PGPException, IOException { OpenPgpMessageInputStream in = get(armoredMessage, options); ByteArrayOutputStream out = new ByteArrayOutputStream(); Streams.pipeAll(in, out); @@ -89,6 +404,19 @@ public class OpenPgpMessageInputStreamTest { return out.toString(); } + private String processReadSequential(String armoredMessage, ConsumerOptions options) throws PGPException, IOException { + OpenPgpMessageInputStream in = get(armoredMessage, options); + ByteArrayOutputStream out = new ByteArrayOutputStream(); + + int r; + while ((r = in.read()) != -1) { + out.write(r); + } + + in.close(); + return out.toString(); + } + private OpenPgpMessageInputStream get(String armored, ConsumerOptions options) throws IOException, PGPException { ByteArrayInputStream bytesIn = new ByteArrayInputStream(armored.getBytes(StandardCharsets.UTF_8)); ArmoredInputStream armorIn = ArmoredInputStreamFactory.get(bytesIn); diff --git a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/PGPDecryptionStreamTest.java b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/PGPDecryptionStreamTest.java deleted file mode 100644 index 8da44ec8..00000000 --- a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/PGPDecryptionStreamTest.java +++ /dev/null @@ -1,361 +0,0 @@ -package org.pgpainless.decryption_verification; - -import org.bouncycastle.bcpg.ArmoredInputStream; -import org.bouncycastle.bcpg.ArmoredOutputStream; -import org.bouncycastle.bcpg.CompressionAlgorithmTags; -import org.bouncycastle.openpgp.PGPCompressedDataGenerator; -import org.bouncycastle.openpgp.PGPException; -import org.bouncycastle.openpgp.PGPLiteralData; -import org.bouncycastle.openpgp.PGPLiteralDataGenerator; -import org.bouncycastle.openpgp.PGPPublicKeyRing; -import org.bouncycastle.openpgp.PGPSecretKeyRing; -import org.bouncycastle.openpgp.PGPSignature; -import org.bouncycastle.util.io.Streams; -import org.junit.jupiter.api.Test; -import org.pgpainless.PGPainless; -import org.pgpainless.algorithm.CompressionAlgorithm; -import org.pgpainless.encryption_signing.EncryptionOptions; -import org.pgpainless.encryption_signing.EncryptionResult; -import org.pgpainless.encryption_signing.EncryptionStream; -import org.pgpainless.encryption_signing.ProducerOptions; -import org.pgpainless.encryption_signing.SigningOptions; -import org.pgpainless.exception.MalformedOpenPgpMessageException; -import org.pgpainless.key.protection.SecretKeyRingProtector; -import org.pgpainless.util.ArmoredInputStreamFactory; -import org.pgpainless.util.Passphrase; - -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.OutputStream; -import java.nio.charset.StandardCharsets; -import java.security.InvalidAlgorithmParameterException; -import java.security.NoSuchAlgorithmException; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertThrows; - -public class PGPDecryptionStreamTest { - - public static final String KEY = "-----BEGIN PGP PRIVATE KEY BLOCK-----\n" + - "Version: PGPainless\n" + - "Comment: DA05 848F 37D4 68E6 F982 C889 7A70 1FC6 904D 3F4C\n" + - "Comment: Alice \n" + - "\n" + - "lFgEYxzSCBYJKwYBBAHaRw8BAQdAeJU8m4GOJb1eQgv/ryilFHRfNLTYFMNqL6zj\n" + - "r0vF7dsAAP42rAtngpJ6dZxoZlJX0Je65zk1VMPeTrXaWfPS2HSKBRGptBxBbGlj\n" + - "ZSA8YWxpY2VAcGdwYWlubGVzcy5vcmc+iI8EExYKAEEFAmMc0ggJEHpwH8aQTT9M\n" + - "FiEE2gWEjzfUaOb5gsiJenAfxpBNP0wCngECmwEFFgIDAQAECwkIBwUVCgkICwKZ\n" + - "AQAApZEBALUXHtvswPZG28YO+16Men6/fpk+scvqpNMnD4ty3IkAAPwK6TuXjNnZ\n" + - "0XuWdnilvLMV23Ai1d5g6em+lwLK5M2SApxdBGMc0ggSCisGAQQBl1UBBQEBB0D8\n" + - "mNUVX8y2MXFaSeFYqOTPFnGT7dgNVdn6yc0UtkkHOgMBCAcAAP9y9OtP4SX9voPb\n" + - "ID2u9PkJKgo4hTB8NK5LouGppdRtEBGriHUEGBYKAB0FAmMc0ggCngECmwwFFgID\n" + - "AQAECwkIBwUVCgkICwAKCRB6cB/GkE0/TAywAQDpZRJS/joFH4+xcwheqWfI7ay/\n" + - "WfojUoGQMYGnUjsgYwEAkceRUsgkqI0SVgYvuglfaQpZ9k2ns1mZGVLkXvu/yQyc\n" + - "WARjHNIIFgkrBgEEAdpHDwEBB0BGN9BybSOrj8B6gim1SjbB/IiqAshlzMDunVkQ\n" + - "X23npQABAJqvjOOY7qhBuTusC5/Q5+25iLrhMn4TI+LXlJHMVNOaE0OI1QQYFgoA\n" + - "fQUCYxzSCAKeAQKbAgUWAgMBAAQLCQgHBRUKCQgLXyAEGRYKAAYFAmMc0ggACgkQ\n" + - "KALh4BJQXl6yTQD/dh0N5228Uwtu7XHy6dmpMRX62cac5tXQ9WaDzpy8STgBAMdn\n" + - "Mq948UOYEhdk/ZY2/hwux/4t+FHvqrXW8ziBe4cLAAoJEHpwH8aQTT9M1hQA/3Ms\n" + - "P3kzoed3VsWu1ZMr7dKEngbc6SoJ2XPayzN0QYJaAQCIY5NcT9mZF97HWV3Vgeum\n" + - "00sWMHXfkW3+nl5OpUZaDA==\n" + - "=THgv\n" + - "-----END PGP PRIVATE KEY BLOCK-----"; - - public static final String PLAINTEXT = "Hello, World!\n"; - public static final String PASSPHRASE = "sw0rdf1sh"; - - public static final String LIT = "" + - "-----BEGIN PGP MESSAGE-----\n" + - "Version: PGPainless\n" + - "\n" + - "yxRiAAAAAABIZWxsbywgV29ybGQhCg==\n" + - "=WGju\n" + - "-----END PGP MESSAGE-----"; - - public static final String LIT_LIT = "" + - "-----BEGIN PGP MESSAGE-----\n" + - "Version: BCPG v1.71\n" + - "\n" + - "yxRiAAAAAABIZWxsbywgV29ybGQhCssUYgAAAAAASGVsbG8sIFdvcmxkIQo=\n" + - "=A91Q\n" + - "-----END PGP MESSAGE-----"; - - public static final String COMP_LIT = "" + - "-----BEGIN PGP MESSAGE-----\n" + - "Version: BCPG v1.71\n" + - "\n" + - "owE7LZLEAAIeqTk5+ToK4flFOSmKXAA=\n" + - "=ZYDg\n" + - "-----END PGP MESSAGE-----"; - - public static final String COMP = "" + - "-----BEGIN PGP MESSAGE-----\n" + - "Version: BCPG v1.71\n" + - "\n" + - "owEDAA==\n" + - "=MDzg\n" + - "-----END PGP MESSAGE-----"; - - public static final String COMP_COMP_LIT = "" + - "-----BEGIN PGP MESSAGE-----\n" + - "Version: BCPG v1.71\n" + - "\n" + - "owEBRwC4/6MDQlpoOTFBWSZTWVuW2KAAAAr3hGAQBABgBABAAIAWBJAAAAggADFM\n" + - "ABNBqBo00N6puqWR+TqInoXQ58XckU4UJBbltigA\n" + - "=K9Zl\n" + - "-----END PGP MESSAGE-----"; - - public static final String SIG_LIT = "" + - "-----BEGIN PGP MESSAGE-----\n" + - "Version: BCPG v1.71\n" + - "\n" + - "iHUEABYKACcFAmMc1i0JECgC4eASUF5eFiEEjN3RiJxCf/TyYOQjKALh4BJQXl4A\n" + - "AHkrAP98uPpqrgIix7epgL7MM1cjXXGSxqbDfXHwgptk1YGQlgD/fw89VGcXwFaI\n" + - "2k7kpXQYy/1BqnovM/jZ3X3mXhhTaAOjATstksQAAh6pOTn5Ogrh+UU5KYpcAA==\n" + - "=WKPn\n" + - "-----END PGP MESSAGE-----"; - - public static final String SENC_LIT = "" + - "-----BEGIN PGP MESSAGE-----\n" + - "Version: PGPainless\n" + - "\n" + - "jA0ECQMCuZ0qHNXWnGhg0j8Bdm1cxV65sYb7jDgb4rRMtdNpQ1dC4UpSYuk9YWS2\n" + - "DpNEijbX8b/P1UOK2kJczNDADMRegZuLEI+dNsBnJjk=\n" + - "=i4Y0\n" + - "-----END PGP MESSAGE-----"; - - public static final String PENC_COMP_LIT = "" + - "-----BEGIN PGP MESSAGE-----\n" + - "Version: PGPainless\n" + - "\n" + - "hF4Dyqa/GWUy6WsSAQdAQ62BwmUt8Iby0+jvrLhMgST79KR/as+dyl0nf1uki2sw\n" + - "Thg1Ojtf0hOyJgcpQ4nP2Q0wYFR0F1sCydaIlTGreYZHlGtybP7/Ml6KNZILTRWP\n" + - "0kYBkGBgK7oQWRIVyoF2POvEP6EX1X8nvQk7O3NysVdRVbnia7gE3AzRYuha4kxs\n" + - "pI6xJkntLMS3K6him1Y9FHINIASFSB+C\n" + - "=5p00\n" + - "-----END PGP MESSAGE-----"; - - @Test - public void genLIT() throws IOException { - ArmoredOutputStream armorOut = new ArmoredOutputStream(System.out); - PGPLiteralDataGenerator litGen = new PGPLiteralDataGenerator(); - OutputStream litOut = litGen.open(armorOut, PGPLiteralDataGenerator.BINARY, "", PGPLiteralData.NOW, new byte[1 << 9]); - litOut.write(PLAINTEXT.getBytes(StandardCharsets.UTF_8)); - litOut.close(); - armorOut.close(); - } - - @Test - public void processLIT() throws IOException, PGPException { - ByteArrayInputStream bytesIn = new ByteArrayInputStream(LIT.getBytes(StandardCharsets.UTF_8)); - ArmoredInputStream armorIn = ArmoredInputStreamFactory.get(bytesIn); - MessageDecryptionStream decIn = new MessageDecryptionStream(armorIn, ConsumerOptions.get()); - - ByteArrayOutputStream out = new ByteArrayOutputStream(); - Streams.pipeAll(decIn, out); - assertEquals(PLAINTEXT, out.toString()); - armorIn.close(); - } - - @Test - public void getLIT_LIT() throws IOException { - ArmoredOutputStream armorOut = new ArmoredOutputStream(System.out); - PGPLiteralDataGenerator litGen = new PGPLiteralDataGenerator(); - OutputStream litOut = litGen.open(armorOut, PGPLiteralDataGenerator.BINARY, "", PGPLiteralData.NOW, new byte[1 << 9]); - litOut.write(PLAINTEXT.getBytes(StandardCharsets.UTF_8)); - litOut.close(); - - litOut = litGen.open(armorOut, PGPLiteralDataGenerator.BINARY, "", PGPLiteralData.NOW, new byte[1 << 9]); - litOut.write(PLAINTEXT.getBytes(StandardCharsets.UTF_8)); - litOut.close(); - - armorOut.close(); - } - - @Test - public void processLIT_LIT() throws IOException, PGPException { - ByteArrayInputStream bytesIn = new ByteArrayInputStream(LIT_LIT.getBytes(StandardCharsets.UTF_8)); - ArmoredInputStream armorIn = ArmoredInputStreamFactory.get(bytesIn); - MessageDecryptionStream decIn = new MessageDecryptionStream(armorIn, ConsumerOptions.get()); - - ByteArrayOutputStream out = new ByteArrayOutputStream(); - assertThrows(MalformedOpenPgpMessageException.class, () -> Streams.pipeAll(decIn, out)); - } - - @Test - public void genCOMP_LIT() throws IOException { - ArmoredOutputStream armorOut = new ArmoredOutputStream(System.out); - PGPCompressedDataGenerator compGen = new PGPCompressedDataGenerator(CompressionAlgorithmTags.ZIP); - OutputStream compOut = compGen.open(armorOut); - PGPLiteralDataGenerator litGen = new PGPLiteralDataGenerator(); - OutputStream litOut = litGen.open(compOut, PGPLiteralDataGenerator.BINARY, "", PGPLiteralData.NOW, new byte[1 << 9]); - litOut.write(PLAINTEXT.getBytes(StandardCharsets.UTF_8)); - litOut.close(); - compOut.close(); - armorOut.close(); - } - - @Test - public void processCOMP_LIT() throws IOException, PGPException { - ByteArrayInputStream bytesIn = new ByteArrayInputStream(COMP_LIT.getBytes(StandardCharsets.UTF_8)); - ArmoredInputStream armorIn = ArmoredInputStreamFactory.get(bytesIn); - MessageDecryptionStream decIn = new MessageDecryptionStream(armorIn, ConsumerOptions.get()); - - ByteArrayOutputStream out = new ByteArrayOutputStream(); - Streams.pipeAll(decIn, out); - decIn.close(); - armorIn.close(); - - assertEquals(PLAINTEXT, out.toString()); - } - - @Test - public void genCOMP() throws IOException { - ArmoredOutputStream armorOut = new ArmoredOutputStream(System.out); - PGPCompressedDataGenerator compGen = new PGPCompressedDataGenerator(CompressionAlgorithmTags.ZIP); - OutputStream compOut = compGen.open(armorOut); - compOut.close(); - armorOut.close(); - } - - @Test - public void processCOMP() throws IOException { - ByteArrayInputStream bytesIn = new ByteArrayInputStream(COMP.getBytes(StandardCharsets.UTF_8)); - ArmoredInputStream armorIn = ArmoredInputStreamFactory.get(bytesIn); - assertThrows(MalformedOpenPgpMessageException.class, () -> { - MessageDecryptionStream decIn = new MessageDecryptionStream(armorIn, ConsumerOptions.get()); - Streams.drain(decIn); - }); - } - - @Test - public void genCOMP_COMP_LIT() throws IOException { - ArmoredOutputStream armorOut = new ArmoredOutputStream(System.out); - - PGPCompressedDataGenerator compGen1 = new PGPCompressedDataGenerator(CompressionAlgorithmTags.ZIP); - OutputStream compOut1 = compGen1.open(armorOut); - - PGPCompressedDataGenerator compGen2 = new PGPCompressedDataGenerator(CompressionAlgorithmTags.BZIP2); - OutputStream compOut2 = compGen2.open(compOut1); - - PGPLiteralDataGenerator litGen = new PGPLiteralDataGenerator(); - OutputStream litOut = litGen.open(compOut2, PGPLiteralDataGenerator.BINARY, "", PGPLiteralDataGenerator.NOW, new byte[1 << 9]); - - litOut.write(PLAINTEXT.getBytes(StandardCharsets.UTF_8)); - litOut.close(); - compOut2.close(); - compOut1.close(); - armorOut.close(); - } - - @Test - public void processCOMP_COMP_LIT() throws PGPException, IOException { - ByteArrayInputStream bytesIn = new ByteArrayInputStream(COMP_COMP_LIT.getBytes(StandardCharsets.UTF_8)); - ArmoredInputStream armorIn = ArmoredInputStreamFactory.get(bytesIn); - MessageDecryptionStream decIn = new MessageDecryptionStream(armorIn, ConsumerOptions.get()); - - ByteArrayOutputStream out = new ByteArrayOutputStream(); - Streams.pipeAll(decIn, out); - decIn.close(); - - assertEquals(PLAINTEXT, out.toString()); - } - - @Test - public void genKey() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, IOException { - System.out.println(PGPainless.asciiArmor( - PGPainless.generateKeyRing().modernKeyRing("Alice ") - )); - } - - @Test - public void genSIG_LIT() throws PGPException, IOException { - PGPSecretKeyRing secretKeys = PGPainless.readKeyRing().secretKeyRing(KEY); - ByteArrayOutputStream msgOut = new ByteArrayOutputStream(); - EncryptionStream signer = PGPainless.encryptAndOrSign() - .onOutputStream(msgOut) - .withOptions( - ProducerOptions.sign( - SigningOptions.get() - .addDetachedSignature(SecretKeyRingProtector.unprotectedKeys(), secretKeys) - ).setAsciiArmor(false) - ); - - Streams.pipeAll(new ByteArrayInputStream(PLAINTEXT.getBytes(StandardCharsets.UTF_8)), signer); - signer.close(); - EncryptionResult result = signer.getResult(); - PGPSignature detachedSignature = result.getDetachedSignatures().get(result.getDetachedSignatures().keySet().iterator().next()).iterator().next(); - - ByteArrayOutputStream out = new ByteArrayOutputStream(); - ArmoredOutputStream armorOut = new ArmoredOutputStream(out); - armorOut.flush(); - detachedSignature.encode(armorOut); - armorOut.write(msgOut.toByteArray()); - armorOut.close(); - - String armored = out.toString(); - System.out.println(armored - .replace("-----BEGIN PGP SIGNATURE-----\n", "-----BEGIN PGP MESSAGE-----\n") - .replace("-----END PGP SIGNATURE-----", "-----END PGP MESSAGE-----")); - } - - @Test - public void processSIG_LIT() throws IOException, PGPException { - ByteArrayInputStream bytesIn = new ByteArrayInputStream(SIG_LIT.getBytes(StandardCharsets.UTF_8)); - ArmoredInputStream armorIn = ArmoredInputStreamFactory.get(bytesIn); - MessageDecryptionStream decIn = new MessageDecryptionStream(armorIn, ConsumerOptions.get()); - - ByteArrayOutputStream out = new ByteArrayOutputStream(); - Streams.pipeAll(decIn, out); - decIn.close(); - - System.out.println(out); - } - - @Test - public void genSENC_LIT() throws PGPException, IOException { - ByteArrayOutputStream out = new ByteArrayOutputStream(); - EncryptionStream enc = PGPainless.encryptAndOrSign() - .onOutputStream(out) - .withOptions(ProducerOptions.encrypt(EncryptionOptions.get() - .addPassphrase(Passphrase.fromPassword(PASSPHRASE))) - .overrideCompressionAlgorithm(CompressionAlgorithm.UNCOMPRESSED)); - enc.write(PLAINTEXT.getBytes(StandardCharsets.UTF_8)); - enc.close(); - - System.out.println(out); - } - - @Test - public void processSENC_LIT() throws IOException, PGPException { - ByteArrayInputStream bytesIn = new ByteArrayInputStream(SENC_LIT.getBytes(StandardCharsets.UTF_8)); - ArmoredInputStream armorIn = ArmoredInputStreamFactory.get(bytesIn); - MessageDecryptionStream decIn = new MessageDecryptionStream(armorIn, ConsumerOptions.get() - .addDecryptionPassphrase(Passphrase.fromPassword(PASSPHRASE))); - - ByteArrayOutputStream out = new ByteArrayOutputStream(); - Streams.pipeAll(decIn, out); - decIn.close(); - - System.out.println(out); - } - - @Test - public void genPENC_COMP_LIT() throws IOException, PGPException { - PGPSecretKeyRing secretKeys = PGPainless.readKeyRing().secretKeyRing(KEY); - PGPPublicKeyRing cert = PGPainless.extractCertificate(secretKeys); - - ByteArrayOutputStream out = new ByteArrayOutputStream(); - EncryptionStream enc = PGPainless.encryptAndOrSign() - .onOutputStream(out) - .withOptions(ProducerOptions.encrypt(EncryptionOptions.get() - .addRecipient(cert)) - .overrideCompressionAlgorithm(CompressionAlgorithm.ZLIB)); - - Streams.pipeAll(new ByteArrayInputStream(PLAINTEXT.getBytes(StandardCharsets.UTF_8)), enc); - enc.close(); - - System.out.println(out); - } -} diff --git a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/automaton/NestingPDATest.java b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/automaton/NestingPDATest.java deleted file mode 100644 index 8c1c4921..00000000 --- a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/automaton/NestingPDATest.java +++ /dev/null @@ -1,205 +0,0 @@ -// SPDX-FileCopyrightText: 2022 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.decryption_verification.automaton; - -import org.junit.jupiter.api.Test; -import org.pgpainless.exception.MalformedOpenPgpMessageException; - -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.assertTrue; - -public class NestingPDATest { - - /** - * MSG is valid. - * - * @throws MalformedOpenPgpMessageException fail - */ - @Test - public void testSimpleLiteralMessageIsValid() throws MalformedOpenPgpMessageException { - NestingPDA automaton = new NestingPDA(); - automaton.next(InputAlphabet.LiteralData); - automaton.next(InputAlphabet.EndOfSequence); - - assertTrue(automaton.isValid()); - } - - /** - * OPS MSG SIG is valid. - * - * @throws MalformedOpenPgpMessageException fail - */ - @Test - public void testSimpleOpsSignedMesssageIsValid() throws MalformedOpenPgpMessageException { - NestingPDA automaton = new NestingPDA(); - automaton.next(InputAlphabet.OnePassSignatures); - automaton.next(InputAlphabet.LiteralData); - automaton.next(InputAlphabet.Signatures); - automaton.next(InputAlphabet.EndOfSequence); - - assertTrue(automaton.isValid()); - } - - /** - * SIG MSG is valid. - * - * @throws MalformedOpenPgpMessageException fail - */ - @Test - public void testSimplePrependSignedMessageIsValid() throws MalformedOpenPgpMessageException { - NestingPDA automaton = new NestingPDA(); - automaton.next(InputAlphabet.Signatures); - automaton.next(InputAlphabet.LiteralData); - automaton.next(InputAlphabet.EndOfSequence); - - assertTrue(automaton.isValid()); - } - - /** - * OPS COMP(MSG) SIG is valid. - * - * @throws MalformedOpenPgpMessageException fail - */ - @Test - public void testOPSSignedCompressedMessageIsValid() throws MalformedOpenPgpMessageException { - NestingPDA automaton = new NestingPDA(); - automaton.next(InputAlphabet.OnePassSignatures); - automaton.next(InputAlphabet.CompressedData); - automaton.next(InputAlphabet.LiteralData); - automaton.next(InputAlphabet.EndOfSequence); - automaton.next(InputAlphabet.Signatures); - automaton.next(InputAlphabet.EndOfSequence); - - assertTrue(automaton.isValid()); - } - - /** - * OPS ENC(COMP(COMP(MSG))) SIG is valid. - * - * @throws MalformedOpenPgpMessageException fail - */ - @Test - public void testOpsSignedEncryptedCompressedCompressedMessageIsValid() throws MalformedOpenPgpMessageException { - NestingPDA automaton = new NestingPDA(); - automaton.next(InputAlphabet.OnePassSignatures); - automaton.next(InputAlphabet.EncryptedData); - automaton.next(InputAlphabet.CompressedData); - automaton.next(InputAlphabet.CompressedData); - - automaton.next(InputAlphabet.LiteralData); - - automaton.next(InputAlphabet.EndOfSequence); - automaton.next(InputAlphabet.EndOfSequence); - automaton.next(InputAlphabet.EndOfSequence); - automaton.next(InputAlphabet.Signatures); - automaton.next(InputAlphabet.EndOfSequence); - - assertTrue(automaton.isValid()); - } - - /** - * MSG SIG is invalid. - * - * @throws MalformedOpenPgpMessageException fail - */ - @Test - public void testLiteralPlusSigsFails() throws MalformedOpenPgpMessageException { - NestingPDA automaton = new NestingPDA(); - automaton.next(InputAlphabet.LiteralData); - assertThrows(MalformedOpenPgpMessageException.class, - () -> automaton.next(InputAlphabet.Signatures)); - } - - /** - * MSG MSG is invalid. - * - * @throws MalformedOpenPgpMessageException fail - */ - @Test - public void testTwoLiteralDataPacketsFails() throws MalformedOpenPgpMessageException { - NestingPDA automaton = new NestingPDA(); - automaton.next(InputAlphabet.LiteralData); - assertThrows(MalformedOpenPgpMessageException.class, - () -> automaton.next(InputAlphabet.LiteralData)); - } - - /** - * OPS COMP(MSG MSG) SIG is invalid (two literal packets are illegal). - * - * @throws MalformedOpenPgpMessageException fail - */ - @Test - public void testOPSSignedMessageWithTwoLiteralDataPacketsFails() throws MalformedOpenPgpMessageException { - NestingPDA automaton = new NestingPDA(); - automaton.next(InputAlphabet.OnePassSignatures); - automaton.next(InputAlphabet.CompressedData); - automaton.next(InputAlphabet.LiteralData); - assertThrows(MalformedOpenPgpMessageException.class, - () -> automaton.next(InputAlphabet.LiteralData)); - } - - /** - * OPS COMP(MSG) MSG SIG is invalid. - * - * @throws MalformedOpenPgpMessageException fail - */ - @Test - public void testOPSSignedMessageWithTwoLiteralDataPacketsFails2() throws MalformedOpenPgpMessageException { - NestingPDA automaton = new NestingPDA(); - automaton.next(InputAlphabet.OnePassSignatures); - automaton.next(InputAlphabet.CompressedData); - automaton.next(InputAlphabet.LiteralData); - automaton.next(InputAlphabet.EndOfSequence); - assertThrows(MalformedOpenPgpMessageException.class, - () -> automaton.next(InputAlphabet.LiteralData)); - } - - /** - * OPS COMP(MSG SIG) is invalid (MSG SIG does not form valid nested message). - * - * @throws MalformedOpenPgpMessageException fail - */ - @Test - public void testCorrespondingSignaturesOfOpsSignedMessageAreLayerFurtherDownFails() throws MalformedOpenPgpMessageException { - NestingPDA automaton = new NestingPDA(); - automaton.next(InputAlphabet.OnePassSignatures); - automaton.next(InputAlphabet.CompressedData); - automaton.next(InputAlphabet.LiteralData); - assertThrows(MalformedOpenPgpMessageException.class, - () -> automaton.next(InputAlphabet.Signatures)); - } - - /** - * Empty COMP is invalid. - */ - @Test - public void testEmptyCompressedDataIsInvalid() throws MalformedOpenPgpMessageException { - NestingPDA automaton = new NestingPDA(); - automaton.next(InputAlphabet.CompressedData); - assertThrows(MalformedOpenPgpMessageException.class, - () -> automaton.next(InputAlphabet.EndOfSequence)); - } - - @Test - public void testOPSSignedEncryptedCompressedOPSSignedMessageIsValid() throws MalformedOpenPgpMessageException { - NestingPDA automaton = new NestingPDA(); - automaton.next(InputAlphabet.OnePassSignatures); - - automaton.next(InputAlphabet.EncryptedData); - automaton.next(InputAlphabet.OnePassSignatures); - - automaton.next(InputAlphabet.CompressedData); - automaton.next(InputAlphabet.LiteralData); - automaton.next(InputAlphabet.EndOfSequence); - - automaton.next(InputAlphabet.Signatures); - automaton.next(InputAlphabet.EndOfSequence); - - automaton.next(InputAlphabet.Signatures); - automaton.next(InputAlphabet.EndOfSequence); - - assertTrue(automaton.isValid()); - } -} From 7537c9520c07fd3499f683a37b6f651f5f6b8549 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 19 Sep 2022 13:07:33 +0200 Subject: [PATCH 022/763] WIP: Add LayerMetadata class --- .../pgpainless/algorithm/OpenPgpPacket.java | 2 +- .../OpenPgpMessageInputStream.java | 271 ++++++++++-------- 2 files changed, 149 insertions(+), 124 deletions(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/algorithm/OpenPgpPacket.java b/pgpainless-core/src/main/java/org/pgpainless/algorithm/OpenPgpPacket.java index 63d14a31..41e3fb08 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/algorithm/OpenPgpPacket.java +++ b/pgpainless-core/src/main/java/org/pgpainless/algorithm/OpenPgpPacket.java @@ -29,7 +29,7 @@ public enum OpenPgpPacket { PSK(PacketTags.PUBLIC_SUBKEY), UATTR(PacketTags.USER_ATTRIBUTE), SEIPD(PacketTags.SYM_ENC_INTEGRITY_PRO), - MOD(PacketTags.MOD_DETECTION_CODE), + MDC(PacketTags.MOD_DETECTION_CODE), EXP_1(PacketTags.EXPERIMENTAL_1), EXP_2(PacketTags.EXPERIMENTAL_2), diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java index 8dff7189..1a38fced 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java @@ -1,3 +1,7 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + package org.pgpainless.decryption_verification; import org.bouncycastle.bcpg.BCPGInputStream; @@ -27,9 +31,12 @@ import org.bouncycastle.openpgp.operator.PBEDataDecryptorFactory; import org.bouncycastle.openpgp.operator.PGPContentVerifierBuilderProvider; import org.bouncycastle.openpgp.operator.PublicKeyDataDecryptorFactory; import org.bouncycastle.openpgp.operator.SessionKeyDataDecryptorFactory; +import org.bouncycastle.pqc.crypto.rainbow.Layer; import org.pgpainless.PGPainless; +import org.pgpainless.algorithm.CompressionAlgorithm; import org.pgpainless.algorithm.EncryptionPurpose; import org.pgpainless.algorithm.OpenPgpPacket; +import org.pgpainless.algorithm.SymmetricKeyAlgorithm; import org.pgpainless.decryption_verification.automaton.InputAlphabet; import org.pgpainless.decryption_verification.automaton.PDA; import org.pgpainless.decryption_verification.automaton.StackAlphabet; @@ -55,15 +62,22 @@ public class OpenPgpMessageInputStream extends InputStream { protected final PDA automaton = new PDA(); protected final ConsumerOptions options; + protected final OpenPgpMetadata.Builder resultBuilder; protected final BCPGInputStream bcpgIn; protected InputStream in; private boolean closed = false; private Signatures signatures; + private LayerMetadata layerMetadata; public OpenPgpMessageInputStream(InputStream inputStream, ConsumerOptions options) throws IOException, PGPException { + this(inputStream, options, null); + } + + OpenPgpMessageInputStream(InputStream inputStream, ConsumerOptions options, LayerMetadata layerMetadata) + throws PGPException, IOException { // TODO: Use BCPGInputStream.wrap(inputStream); if (inputStream instanceof BCPGInputStream) { this.bcpgIn = (BCPGInputStream) inputStream; @@ -72,12 +86,35 @@ public class OpenPgpMessageInputStream extends InputStream { } this.options = options; + this.resultBuilder = OpenPgpMetadata.getBuilder(); this.signatures = new Signatures(options); this.signatures.addDetachedSignatures(options.getDetachedSignatures()); consumePackets(); } + static class LayerMetadata { + + private CompressionAlgorithm compressionAlgorithm; + private SymmetricKeyAlgorithm symmetricKeyAlgorithm; + private LayerMetadata child; + + public LayerMetadata setCompressionAlgorithm(CompressionAlgorithm algorithm) { + this.compressionAlgorithm = algorithm; + return this; + } + + public LayerMetadata setSymmetricEncryptionAlgorithm(SymmetricKeyAlgorithm algorithm) { + this.symmetricKeyAlgorithm = algorithm; + return this; + } + + public LayerMetadata setChild(LayerMetadata child) { + this.child = child; + return this; + } + } + /** * This method consumes OpenPGP packets from the current {@link BCPGInputStream}. * Once it reaches a "nested" OpenPGP packet (Literal Data, Compressed Data, Encrypted Data), it sets

in
@@ -92,7 +129,7 @@ public class OpenPgpMessageInputStream extends InputStream { throws IOException, PGPException { System.out.println("Walk " + automaton); int tag; - loop: while ((tag = getTag()) != -1) { + loop: while ((tag = nextTag()) != -1) { OpenPgpPacket nextPacket = OpenPgpPacket.requireFromTag(tag); System.out.println(nextPacket); switch (nextPacket) { @@ -108,7 +145,9 @@ public class OpenPgpMessageInputStream extends InputStream { case COMP: automaton.next(InputAlphabet.CompressedData); PGPCompressedData compressedData = new PGPCompressedData(bcpgIn); - in = new OpenPgpMessageInputStream(compressedData.getDataStream(), options); + LayerMetadata compressionLayer = new LayerMetadata(); + compressionLayer.setCompressionAlgorithm(CompressionAlgorithm.fromId(compressedData.getAlgorithm())); + in = new OpenPgpMessageInputStream(compressedData.getDataStream(), options, compressionLayer); break loop; // One Pass Signatures @@ -119,10 +158,10 @@ public class OpenPgpMessageInputStream extends InputStream { // Signatures - either prepended to the message, or corresponding to the One Pass Signatures case SIG: - boolean isCorrespondingToOPS = automaton.peekStack() == StackAlphabet.ops; + boolean isSigForOPS = automaton.peekStack() == StackAlphabet.ops; automaton.next(InputAlphabet.Signatures); PGPSignatureList signatureList = readSignatures(); - if (isCorrespondingToOPS) { + if (isSigForOPS) { signatures.addOnePassCorrespondingSignatures(signatureList); } else { signatures.addPrependedSignatures(signatureList); @@ -135,99 +174,15 @@ public class OpenPgpMessageInputStream extends InputStream { case SED: case SEIPD: automaton.next(InputAlphabet.EncryptedData); - PGPEncryptedDataList encDataList = new PGPEncryptedDataList(bcpgIn); - - // TODO: Replace with !encDataList.isIntegrityProtected() - if (!encDataList.get(0).isIntegrityProtected()) { - throw new MessageNotIntegrityProtectedException(); + if (processEncryptedData()) { + break loop; } - SortedESKs esks = new SortedESKs(encDataList); - - if (options.getSessionKey() != null) { - SessionKeyDataDecryptorFactory decryptorFactory = ImplementationFactory.getInstance() - .getSessionKeyDataDecryptorFactory(options.getSessionKey()); - // TODO: Replace with encDataList.addSessionKeyDecryptionMethod(sessionKey) - PGPEncryptedData esk = esks.all().get(0); - try { - if (esk instanceof PGPPBEEncryptedData) { - PGPPBEEncryptedData skesk = (PGPPBEEncryptedData) esk; - in = skesk.getDataStream(decryptorFactory); - break loop; - } else if (esk instanceof PGPPublicKeyEncryptedData) { - PGPPublicKeyEncryptedData pkesk = (PGPPublicKeyEncryptedData) esk; - in = pkesk.getDataStream(decryptorFactory); - break loop; - } else { - throw new RuntimeException("Unknown ESK class type: " + esk.getClass().getName()); - } - } catch (PGPException e) { - // Session key mismatch? - } - } - - // Try passwords - for (PGPPBEEncryptedData skesk : esks.skesks) { - for (Passphrase passphrase : options.getDecryptionPassphrases()) { - PBEDataDecryptorFactory decryptorFactory = ImplementationFactory.getInstance() - .getPBEDataDecryptorFactory(passphrase); - try { - InputStream decrypted = skesk.getDataStream(decryptorFactory); - in = new OpenPgpMessageInputStream(decrypted, options); - break loop; - } catch (PGPException e) { - // password mismatch? Try next password - } - - } - } - - // Try (known) secret keys - for (PGPPublicKeyEncryptedData pkesk : esks.pkesks) { - long keyId = pkesk.getKeyID(); - PGPSecretKeyRing decryptionKeys = getDecryptionKey(keyId); - if (decryptionKeys == null) { - continue; - } - SecretKeyRingProtector protector = options.getSecretKeyProtector(decryptionKeys); - PGPSecretKey decryptionKey = decryptionKeys.getSecretKey(keyId); - PGPPrivateKey privateKey = UnlockSecretKey.unlockSecretKey(decryptionKey, protector); - - PublicKeyDataDecryptorFactory decryptorFactory = ImplementationFactory.getInstance() - .getPublicKeyDataDecryptorFactory(privateKey); - try { - InputStream decrypted = pkesk.getDataStream(decryptorFactory); - in = new OpenPgpMessageInputStream(decrypted, options); - break loop; - } catch (PGPException e) { - // hm :/ - } - } - - // try anonymous secret keys - for (PGPPublicKeyEncryptedData pkesk : esks.anonPkesks) { - for (Tuple decryptionKeyCandidate : findPotentialDecryptionKeys(pkesk)) { - SecretKeyRingProtector protector = options.getSecretKeyProtector(decryptionKeyCandidate.getA()); - PGPPrivateKey privateKey = UnlockSecretKey.unlockSecretKey(decryptionKeyCandidate.getB(), protector); - PublicKeyDataDecryptorFactory decryptorFactory = ImplementationFactory.getInstance() - .getPublicKeyDataDecryptorFactory(privateKey); - - try { - InputStream decrypted = pkesk.getDataStream(decryptorFactory); - in = new OpenPgpMessageInputStream(decrypted, options); - break loop; - } catch (PGPException e) { - // hm :/ - } - } - } - - // TODO: try interactive password callbacks - throw new MissingDecryptionMethodException("No working decryption method found."); + // Marker Packets need to be skipped and ignored case MARKER: - bcpgIn.readPacket(); // skip marker packet + bcpgIn.readPacket(); // skip break; // Key Packets are illegal in this context @@ -238,8 +193,11 @@ public class OpenPgpMessageInputStream extends InputStream { case TRUST: case UID: case UATTR: + throw new MalformedOpenPgpMessageException("Illegal Packet in Stream: " + nextPacket); - case MOD: + // MDC packet is usually processed by PGPEncryptedDataList, so it is very likely we encounter this + // packet out of order + case MDC: throw new MalformedOpenPgpMessageException("Unexpected Packet in Stream: " + nextPacket); // Experimental Packets are not supported @@ -252,7 +210,100 @@ public class OpenPgpMessageInputStream extends InputStream { } } - private int getTag() throws IOException { + private boolean processEncryptedData() throws IOException, PGPException { + PGPEncryptedDataList encDataList = new PGPEncryptedDataList(bcpgIn); + + // TODO: Replace with !encDataList.isIntegrityProtected() + if (!encDataList.get(0).isIntegrityProtected()) { + throw new MessageNotIntegrityProtectedException(); + } + + SortedESKs esks = new SortedESKs(encDataList); + + // Try session key + if (options.getSessionKey() != null) { + SessionKeyDataDecryptorFactory decryptorFactory = ImplementationFactory.getInstance() + .getSessionKeyDataDecryptorFactory(options.getSessionKey()); + // TODO: Replace with encDataList.addSessionKeyDecryptionMethod(sessionKey) + PGPEncryptedData esk = esks.all().get(0); + try { + if (esk instanceof PGPPBEEncryptedData) { + PGPPBEEncryptedData skesk = (PGPPBEEncryptedData) esk; + in = skesk.getDataStream(decryptorFactory); + return true; + } else if (esk instanceof PGPPublicKeyEncryptedData) { + PGPPublicKeyEncryptedData pkesk = (PGPPublicKeyEncryptedData) esk; + in = pkesk.getDataStream(decryptorFactory); + return true; + } else { + throw new RuntimeException("Unknown ESK class type: " + esk.getClass().getName()); + } + } catch (PGPException e) { + // Session key mismatch? + } + } + + // Try passwords + for (PGPPBEEncryptedData skesk : esks.skesks) { + for (Passphrase passphrase : options.getDecryptionPassphrases()) { + PBEDataDecryptorFactory decryptorFactory = ImplementationFactory.getInstance() + .getPBEDataDecryptorFactory(passphrase); + try { + InputStream decrypted = skesk.getDataStream(decryptorFactory); + in = new OpenPgpMessageInputStream(decrypted, options); + return true; + } catch (PGPException e) { + // password mismatch? Try next password + } + + } + } + + // Try (known) secret keys + for (PGPPublicKeyEncryptedData pkesk : esks.pkesks) { + long keyId = pkesk.getKeyID(); + PGPSecretKeyRing decryptionKeys = getDecryptionKey(keyId); + if (decryptionKeys == null) { + continue; + } + SecretKeyRingProtector protector = options.getSecretKeyProtector(decryptionKeys); + PGPSecretKey decryptionKey = decryptionKeys.getSecretKey(keyId); + PGPPrivateKey privateKey = UnlockSecretKey.unlockSecretKey(decryptionKey, protector); + + PublicKeyDataDecryptorFactory decryptorFactory = ImplementationFactory.getInstance() + .getPublicKeyDataDecryptorFactory(privateKey); + try { + InputStream decrypted = pkesk.getDataStream(decryptorFactory); + in = new OpenPgpMessageInputStream(decrypted, options); + return true; + } catch (PGPException e) { + // hm :/ + } + } + + // try anonymous secret keys + for (PGPPublicKeyEncryptedData pkesk : esks.anonPkesks) { + for (Tuple decryptionKeyCandidate : findPotentialDecryptionKeys(pkesk)) { + SecretKeyRingProtector protector = options.getSecretKeyProtector(decryptionKeyCandidate.getA()); + PGPPrivateKey privateKey = UnlockSecretKey.unlockSecretKey(decryptionKeyCandidate.getB(), protector); + PublicKeyDataDecryptorFactory decryptorFactory = ImplementationFactory.getInstance() + .getPublicKeyDataDecryptorFactory(privateKey); + + try { + InputStream decrypted = pkesk.getDataStream(decryptorFactory); + in = new OpenPgpMessageInputStream(decrypted, options); + return true; + } catch (PGPException e) { + // hm :/ + } + } + } + + // we did not yet succeed in decrypting any session key :/ + return false; + } + + private int nextTag() throws IOException { try { return bcpgIn.nextPacketTag(); } catch (IOException e) { @@ -296,7 +347,7 @@ public class OpenPgpMessageInputStream extends InputStream { ByteArrayOutputStream buf = new ByteArrayOutputStream(); BCPGOutputStream bcpgOut = new BCPGOutputStream(buf); int tag; - while ((tag = getTag()) == PacketTags.ONE_PASS_SIGNATURE || tag == PacketTags.MARKER) { + while ((tag = nextTag()) == PacketTags.ONE_PASS_SIGNATURE || tag == PacketTags.MARKER) { Packet packet = bcpgIn.readPacket(); if (tag == PacketTags.ONE_PASS_SIGNATURE) { OnePassSignaturePacket sigPacket = (OnePassSignaturePacket) packet; @@ -313,13 +364,13 @@ public class OpenPgpMessageInputStream extends InputStream { private PGPSignatureList readSignatures() throws IOException { ByteArrayOutputStream buf = new ByteArrayOutputStream(); BCPGOutputStream bcpgOut = new BCPGOutputStream(buf); - int tag = getTag(); + int tag = nextTag(); while (tag == PacketTags.SIGNATURE || tag == PacketTags.MARKER) { Packet packet = bcpgIn.readPacket(); if (tag == PacketTags.SIGNATURE) { SignaturePacket sigPacket = (SignaturePacket) packet; sigPacket.encode(bcpgOut); - tag = getTag(); + tag = nextTag(); } } bcpgOut.close(); @@ -356,20 +407,6 @@ public class OpenPgpMessageInputStream extends InputStream { throw new RuntimeException(e); } signatures.finish(); - /* - if (in instanceof OpenPgpMessageInputStream) { - in.close(); - in = null; - } else { - try { - System.out.println("Read consume"); - consumePackets(); - signatures.finish(); - } catch (PGPException e) { - throw new RuntimeException(e); - } - } - */ } return r; } @@ -394,18 +431,6 @@ public class OpenPgpMessageInputStream extends InputStream { throw new RuntimeException(e); } signatures.finish(); - /* - if (in instanceof OpenPgpMessageInputStream) { - in.close(); - in = null; - } else { - try { - consumePackets(); - } catch (PGPException e) { - throw new RuntimeException(e); - } - } - */ } return r; } From efdf2bca0d5b0fcc08448e074d1c9558ec55ff6e Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Fri, 23 Sep 2022 14:04:44 +0200 Subject: [PATCH 023/763] WIP: Play around with TeeInputStreams --- .../OpenPgpMessageInputStream.java | 33 +++++++++-- .../TeeBCPGInputStream.java | 35 +++++++++++ .../TeeBCPGInputStreamTest.java | 58 +++++++++++++++++++ 3 files changed, 121 insertions(+), 5 deletions(-) create mode 100644 pgpainless-core/src/main/java/org/pgpainless/decryption_verification/TeeBCPGInputStream.java create mode 100644 pgpainless-core/src/test/java/org/pgpainless/decryption_verification/TeeBCPGInputStreamTest.java diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java index 1a38fced..990e628d 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java @@ -343,7 +343,8 @@ public class OpenPgpMessageInputStream extends InputStream { return null; } - private PGPOnePassSignatureList readOnePassSignatures() throws IOException { + private PGPOnePassSignatureListWrapper readOnePassSignatures() throws IOException { + List encapsulating = new ArrayList<>(); ByteArrayOutputStream buf = new ByteArrayOutputStream(); BCPGOutputStream bcpgOut = new BCPGOutputStream(buf); int tag; @@ -351,14 +352,16 @@ public class OpenPgpMessageInputStream extends InputStream { Packet packet = bcpgIn.readPacket(); if (tag == PacketTags.ONE_PASS_SIGNATURE) { OnePassSignaturePacket sigPacket = (OnePassSignaturePacket) packet; - sigPacket.encode(bcpgOut); + byte[] bytes = sigPacket.getEncoded(); + encapsulating.add(bytes[bytes.length - 1] == 1); + bcpgOut.write(bytes); } } bcpgOut.close(); PGPObjectFactory objectFactory = ImplementationFactory.getInstance().getPGPObjectFactory(buf.toByteArray()); PGPOnePassSignatureList signatureList = (PGPOnePassSignatureList) objectFactory.nextObject(); - return signatureList; + return new PGPOnePassSignatureListWrapper(signatureList, encapsulating); } private PGPSignatureList readSignatures() throws IOException { @@ -490,6 +493,26 @@ public class OpenPgpMessageInputStream extends InputStream { } } + /** + * Workaround for BC not exposing, whether an OPS is encapsulating or not. + * TODO: Remove once our PR is merged + * + * @see PR against BC + */ + private static class PGPOnePassSignatureListWrapper { + private final PGPOnePassSignatureList list; + private final List encapsulating; + + public PGPOnePassSignatureListWrapper(PGPOnePassSignatureList signatures, List encapsulating) { + this.list = signatures; + this.encapsulating = encapsulating; + } + + public int size() { + return list.size(); + } + } + private static class Signatures { final ConsumerOptions options; List detachedSignatures = new ArrayList<>(); @@ -521,9 +544,9 @@ public class OpenPgpMessageInputStream extends InputStream { } } - void addOnePassSignatures(PGPOnePassSignatureList signatures) { + void addOnePassSignatures(PGPOnePassSignatureListWrapper signatures) { System.out.println("Adding " + signatures.size() + " OPSs"); - for (PGPOnePassSignature ops : signatures) { + for (PGPOnePassSignature ops : signatures.list) { PGPPublicKeyRing certificate = findCertificate(ops.getKeyID()); initialize(ops, certificate); this.onePassSignatures.add(ops); diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/TeeBCPGInputStream.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/TeeBCPGInputStream.java new file mode 100644 index 00000000..ab24f22e --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/TeeBCPGInputStream.java @@ -0,0 +1,35 @@ +package org.pgpainless.decryption_verification; + +import org.bouncycastle.bcpg.BCPGInputStream; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +public class TeeBCPGInputStream extends BCPGInputStream { + + private final OutputStream out; + + public TeeBCPGInputStream(InputStream in, OutputStream outputStream) { + super(in); + this.out = outputStream; + } + + @Override + public int read() throws IOException { + int r = super.read(); + if (r != -1) { + out.write(r); + } + return r; + } + + @Override + public int read(byte[] buf, int off, int len) throws IOException { + int r = super.read(buf, off, len); + if (r > 0) { + out.write(buf, off, r); + } + return r; + } +} diff --git a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/TeeBCPGInputStreamTest.java b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/TeeBCPGInputStreamTest.java new file mode 100644 index 00000000..765221d3 --- /dev/null +++ b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/TeeBCPGInputStreamTest.java @@ -0,0 +1,58 @@ +package org.pgpainless.decryption_verification; + +import org.bouncycastle.bcpg.ArmoredInputStream; +import org.bouncycastle.bcpg.ArmoredOutputStream; +import org.bouncycastle.bcpg.BCPGInputStream; +import org.bouncycastle.bcpg.Packet; +import org.bouncycastle.openpgp.PGPCompressedData; +import org.bouncycastle.openpgp.PGPException; +import org.junit.jupiter.api.Test; +import org.pgpainless.algorithm.OpenPgpPacket; +import org.pgpainless.util.ArmoredInputStreamFactory; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; + +public class TeeBCPGInputStreamTest { + + private static final String INBAND_SIGNED = "-----BEGIN PGP MESSAGE-----\n" + + "Version: PGPainless\n" + + "\n" + + "owGbwMvMyCUWdXSHvVTUtXbG0yJJDCDgkZqTk6+jEJ5flJOiyNVRysIoxsXAxsqU\n" + + "GDiVjUGRUwCmQUyRRWnOn9Z/PIseF3Yz6cCEL05nZDj1OClo75WVTjNmJPemW6qV\n" + + "6ki//1K1++2s0qTP+0N11O4z/BVLDDdxnmQryS+5VXjBX7/0Hxnm/eqeX6Zum35r\n" + + "M8e7ufwA\n" + + "=RDiy\n" + + "-----END PGP MESSAGE-----"; + + @Test + public void test() throws IOException, PGPException { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + ArmoredOutputStream armorOut = new ArmoredOutputStream(out); + + ByteArrayInputStream bytesIn = new ByteArrayInputStream(INBAND_SIGNED.getBytes(StandardCharsets.UTF_8)); + ArmoredInputStream armorIn = ArmoredInputStreamFactory.get(bytesIn); + BCPGInputStream bcpgIn = new BCPGInputStream(armorIn); + TeeBCPGInputStream teeIn = new TeeBCPGInputStream(bcpgIn, armorOut); + + ByteArrayOutputStream nestedOut = new ByteArrayOutputStream(); + ArmoredOutputStream nestedArmorOut = new ArmoredOutputStream(nestedOut); + + PGPCompressedData compressedData = new PGPCompressedData(teeIn); + InputStream nestedStream = compressedData.getDataStream(); + BCPGInputStream nestedBcpgIn = new BCPGInputStream(nestedStream); + TeeBCPGInputStream nestedTeeIn = new TeeBCPGInputStream(nestedBcpgIn, nestedArmorOut); + + int tag; + while ((tag = nestedTeeIn.nextPacketTag()) != -1) { + System.out.println(OpenPgpPacket.requireFromTag(tag)); + Packet packet = nestedTeeIn.readPacket(); + } + + nestedArmorOut.close(); + System.out.println(nestedOut); + } +} From 5c93eb3705c551edc9d05bfa2a1b159084a66dd5 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 26 Sep 2022 18:21:06 +0200 Subject: [PATCH 024/763] Wip: Introduce MessageMetadata class --- .../MessageMetadata.java | 249 ++++++++++++++++++ .../OpenPgpMessageInputStream.java | 108 ++++---- .../automaton/PDA.java | 3 - .../MessageMetadataTest.java | 84 ++++++ .../OpenPgpMessageInputStreamTest.java | 182 +++++++------ 5 files changed, 498 insertions(+), 128 deletions(-) create mode 100644 pgpainless-core/src/main/java/org/pgpainless/decryption_verification/MessageMetadata.java create mode 100644 pgpainless-core/src/test/java/org/pgpainless/decryption_verification/MessageMetadataTest.java diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/MessageMetadata.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/MessageMetadata.java new file mode 100644 index 00000000..7c09fc8d --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/MessageMetadata.java @@ -0,0 +1,249 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.decryption_verification; + +import org.pgpainless.algorithm.CompressionAlgorithm; +import org.pgpainless.algorithm.StreamEncoding; +import org.pgpainless.algorithm.SymmetricKeyAlgorithm; +import org.pgpainless.util.SessionKey; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.util.ArrayList; +import java.util.Date; +import java.util.Iterator; +import java.util.List; +import java.util.NoSuchElementException; + +public class MessageMetadata { + + protected Message message; + + public MessageMetadata(@Nonnull Message message) { + this.message = message; + } + + public @Nullable SymmetricKeyAlgorithm getEncryptionAlgorithm() { + Iterator algorithms = getEncryptionAlgorithms(); + if (algorithms.hasNext()) { + return algorithms.next(); + } + return null; + } + + public @Nonnull Iterator getEncryptionAlgorithms() { + return new LayerIterator(message) { + @Override + public boolean matches(Nested layer) { + return layer instanceof EncryptedData; + } + + @Override + public SymmetricKeyAlgorithm getProperty(Layer last) { + return ((EncryptedData) last).algorithm; + } + }; + } + + public @Nullable CompressionAlgorithm getCompressionAlgorithm() { + Iterator algorithms = getCompressionAlgorithms(); + if (algorithms.hasNext()) { + return algorithms.next(); + } + return null; + } + + public @Nonnull Iterator getCompressionAlgorithms() { + return new LayerIterator(message) { + @Override + public boolean matches(Nested layer) { + return layer instanceof CompressedData; + } + + @Override + public CompressionAlgorithm getProperty(Layer last) { + return ((CompressedData) last).algorithm; + } + }; + } + + public String getFilename() { + return findLiteralData().getFileName(); + } + + public Date getModificationDate() { + return findLiteralData().getModificationDate(); + } + + public StreamEncoding getFormat() { + return findLiteralData().getFormat(); + } + + private LiteralData findLiteralData() { + Nested nested = message.child; + while (nested.hasNestedChild()) { + Layer layer = (Layer) nested; + nested = layer.child; + } + return (LiteralData) nested; + } + + public static abstract class Layer { + protected final List verifiedSignatures = new ArrayList<>(); + protected final List failedSignatures = new ArrayList<>(); + protected Nested child; + + public Nested getChild() { + return child; + } + + public void setChild(Nested child) { + this.child = child; + } + + public List getVerifiedSignatures() { + return new ArrayList<>(verifiedSignatures); + } + + public List getFailedSignatures() { + return new ArrayList<>(failedSignatures); + } + } + + public interface Nested { + boolean hasNestedChild(); + } + + public static class Message extends Layer { + + } + + public static class LiteralData implements Nested { + protected String fileName; + protected Date modificationDate; + protected StreamEncoding format; + + public LiteralData() { + this("", new Date(0L), StreamEncoding.BINARY); + } + + public LiteralData(String fileName, Date modificationDate, StreamEncoding format) { + this.fileName = fileName; + this.modificationDate = modificationDate; + this.format = format; + } + + public String getFileName() { + return fileName; + } + + public Date getModificationDate() { + return modificationDate; + } + + public StreamEncoding getFormat() { + return format; + } + + @Override + public boolean hasNestedChild() { + return false; + } + } + + public static class CompressedData extends Layer implements Nested { + protected final CompressionAlgorithm algorithm; + + public CompressedData(CompressionAlgorithm zip) { + this.algorithm = zip; + } + + public CompressionAlgorithm getAlgorithm() { + return algorithm; + } + + @Override + public boolean hasNestedChild() { + return true; + } + } + + public static class EncryptedData extends Layer implements Nested { + protected final SymmetricKeyAlgorithm algorithm; + protected SessionKey sessionKey; + protected List recipients; + + public EncryptedData(SymmetricKeyAlgorithm algorithm) { + this.algorithm = algorithm; + } + + public SymmetricKeyAlgorithm getAlgorithm() { + return algorithm; + } + + public SessionKey getSessionKey() { + return sessionKey; + } + + public List getRecipients() { + return new ArrayList<>(recipients); + } + + @Override + public boolean hasNestedChild() { + return true; + } + } + + + private static abstract class LayerIterator implements Iterator { + private Nested current; + Layer last = null; + + public LayerIterator(Message message) { + super(); + this.current = message.child; + if (matches(current)) { + last = (Layer) current; + } + } + + @Override + public boolean hasNext() { + if (last == null) { + findNext(); + } + return last != null; + } + + @Override + public O next() { + if (last == null) { + findNext(); + } + if (last != null) { + O property = getProperty(last); + last = null; + return property; + } + throw new NoSuchElementException(); + } + + private void findNext() { + while (current instanceof Layer) { + current = ((Layer) current).child; + if (matches(current)) { + last = (Layer) current; + break; + } + } + } + + abstract boolean matches(Nested layer); + + abstract O getProperty(Layer last); + } + +} diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java index 990e628d..44ebb27e 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java @@ -31,11 +31,11 @@ import org.bouncycastle.openpgp.operator.PBEDataDecryptorFactory; import org.bouncycastle.openpgp.operator.PGPContentVerifierBuilderProvider; import org.bouncycastle.openpgp.operator.PublicKeyDataDecryptorFactory; import org.bouncycastle.openpgp.operator.SessionKeyDataDecryptorFactory; -import org.bouncycastle.pqc.crypto.rainbow.Layer; import org.pgpainless.PGPainless; import org.pgpainless.algorithm.CompressionAlgorithm; import org.pgpainless.algorithm.EncryptionPurpose; import org.pgpainless.algorithm.OpenPgpPacket; +import org.pgpainless.algorithm.StreamEncoding; import org.pgpainless.algorithm.SymmetricKeyAlgorithm; import org.pgpainless.decryption_verification.automaton.InputAlphabet; import org.pgpainless.decryption_verification.automaton.PDA; @@ -69,14 +69,14 @@ public class OpenPgpMessageInputStream extends InputStream { private boolean closed = false; private Signatures signatures; - private LayerMetadata layerMetadata; + private MessageMetadata.Layer metadata; public OpenPgpMessageInputStream(InputStream inputStream, ConsumerOptions options) throws IOException, PGPException { - this(inputStream, options, null); + this(inputStream, options, new MessageMetadata.Message()); } - OpenPgpMessageInputStream(InputStream inputStream, ConsumerOptions options, LayerMetadata layerMetadata) + OpenPgpMessageInputStream(InputStream inputStream, ConsumerOptions options, MessageMetadata.Layer metadata) throws PGPException, IOException { // TODO: Use BCPGInputStream.wrap(inputStream); if (inputStream instanceof BCPGInputStream) { @@ -86,33 +86,12 @@ public class OpenPgpMessageInputStream extends InputStream { } this.options = options; + this.metadata = metadata; this.resultBuilder = OpenPgpMetadata.getBuilder(); this.signatures = new Signatures(options); this.signatures.addDetachedSignatures(options.getDetachedSignatures()); - consumePackets(); - } - - static class LayerMetadata { - - private CompressionAlgorithm compressionAlgorithm; - private SymmetricKeyAlgorithm symmetricKeyAlgorithm; - private LayerMetadata child; - - public LayerMetadata setCompressionAlgorithm(CompressionAlgorithm algorithm) { - this.compressionAlgorithm = algorithm; - return this; - } - - public LayerMetadata setSymmetricEncryptionAlgorithm(SymmetricKeyAlgorithm algorithm) { - this.symmetricKeyAlgorithm = algorithm; - return this; - } - - public LayerMetadata setChild(LayerMetadata child) { - this.child = child; - return this; - } + consumePackets(); // nom nom nom } /** @@ -127,27 +106,21 @@ public class OpenPgpMessageInputStream extends InputStream { */ private void consumePackets() throws IOException, PGPException { - System.out.println("Walk " + automaton); int tag; loop: while ((tag = nextTag()) != -1) { OpenPgpPacket nextPacket = OpenPgpPacket.requireFromTag(tag); - System.out.println(nextPacket); switch (nextPacket) { // Literal Data - the literal data content is the new input stream case LIT: automaton.next(InputAlphabet.LiteralData); - PGPLiteralData literalData = new PGPLiteralData(bcpgIn); - in = literalData.getDataStream(); + processLiteralData(); break loop; // Compressed Data - the content contains another OpenPGP message case COMP: automaton.next(InputAlphabet.CompressedData); - PGPCompressedData compressedData = new PGPCompressedData(bcpgIn); - LayerMetadata compressionLayer = new LayerMetadata(); - compressionLayer.setCompressionAlgorithm(CompressionAlgorithm.fromId(compressedData.getAlgorithm())); - in = new OpenPgpMessageInputStream(compressedData.getDataStream(), options, compressionLayer); + processCompressedData(); break loop; // One Pass Signatures @@ -160,12 +133,7 @@ public class OpenPgpMessageInputStream extends InputStream { case SIG: boolean isSigForOPS = automaton.peekStack() == StackAlphabet.ops; automaton.next(InputAlphabet.Signatures); - PGPSignatureList signatureList = readSignatures(); - if (isSigForOPS) { - signatures.addOnePassCorrespondingSignatures(signatureList); - } else { - signatures.addPrependedSignatures(signatureList); - } + processSignature(isSigForOPS); break; // Encrypted Data (ESKs and SED/SEIPD are parsed the same by BC) @@ -210,6 +178,29 @@ public class OpenPgpMessageInputStream extends InputStream { } } + private void processSignature(boolean isSigForOPS) throws IOException { + PGPSignatureList signatureList = readSignatures(); + if (isSigForOPS) { + signatures.addOnePassCorrespondingSignatures(signatureList); + } else { + signatures.addPrependedSignatures(signatureList); + } + } + + private void processCompressedData() throws IOException, PGPException { + PGPCompressedData compressedData = new PGPCompressedData(bcpgIn); + MessageMetadata.CompressedData compressionLayer = new MessageMetadata.CompressedData( + CompressionAlgorithm.fromId(compressedData.getAlgorithm())); + in = new OpenPgpMessageInputStream(compressedData.getDataStream(), options, compressionLayer); + } + + private void processLiteralData() throws IOException { + PGPLiteralData literalData = new PGPLiteralData(bcpgIn); + this.metadata.setChild(new MessageMetadata.LiteralData(literalData.getFileName(), literalData.getModificationTime(), + StreamEncoding.requireFromCode(literalData.getFormat()))); + in = literalData.getDataStream(); + } + private boolean processEncryptedData() throws IOException, PGPException { PGPEncryptedDataList encDataList = new PGPEncryptedDataList(bcpgIn); @@ -227,13 +218,14 @@ public class OpenPgpMessageInputStream extends InputStream { // TODO: Replace with encDataList.addSessionKeyDecryptionMethod(sessionKey) PGPEncryptedData esk = esks.all().get(0); try { + MessageMetadata.EncryptedData encryptedData = new MessageMetadata.EncryptedData(options.getSessionKey().getAlgorithm()); if (esk instanceof PGPPBEEncryptedData) { PGPPBEEncryptedData skesk = (PGPPBEEncryptedData) esk; - in = skesk.getDataStream(decryptorFactory); + in = new OpenPgpMessageInputStream(skesk.getDataStream(decryptorFactory), options, encryptedData); return true; } else if (esk instanceof PGPPublicKeyEncryptedData) { PGPPublicKeyEncryptedData pkesk = (PGPPublicKeyEncryptedData) esk; - in = pkesk.getDataStream(decryptorFactory); + in = new OpenPgpMessageInputStream(pkesk.getDataStream(decryptorFactory), options, encryptedData); return true; } else { throw new RuntimeException("Unknown ESK class type: " + esk.getClass().getName()); @@ -250,7 +242,9 @@ public class OpenPgpMessageInputStream extends InputStream { .getPBEDataDecryptorFactory(passphrase); try { InputStream decrypted = skesk.getDataStream(decryptorFactory); - in = new OpenPgpMessageInputStream(decrypted, options); + MessageMetadata.EncryptedData encryptedData = new MessageMetadata.EncryptedData( + SymmetricKeyAlgorithm.requireFromId(skesk.getSymmetricAlgorithm(decryptorFactory))); + in = new OpenPgpMessageInputStream(decrypted, options, encryptedData); return true; } catch (PGPException e) { // password mismatch? Try next password @@ -274,7 +268,9 @@ public class OpenPgpMessageInputStream extends InputStream { .getPublicKeyDataDecryptorFactory(privateKey); try { InputStream decrypted = pkesk.getDataStream(decryptorFactory); - in = new OpenPgpMessageInputStream(decrypted, options); + MessageMetadata.EncryptedData encryptedData = new MessageMetadata.EncryptedData( + SymmetricKeyAlgorithm.requireFromId(pkesk.getSymmetricAlgorithm(decryptorFactory))); + in = new OpenPgpMessageInputStream(decrypted, options, encryptedData); return true; } catch (PGPException e) { // hm :/ @@ -291,7 +287,9 @@ public class OpenPgpMessageInputStream extends InputStream { try { InputStream decrypted = pkesk.getDataStream(decryptorFactory); - in = new OpenPgpMessageInputStream(decrypted, options); + MessageMetadata.EncryptedData encryptedData = new MessageMetadata.EncryptedData( + SymmetricKeyAlgorithm.requireFromId(pkesk.getSymmetricAlgorithm(decryptorFactory))); + in = new OpenPgpMessageInputStream(decrypted, options, encryptedData); return true; } catch (PGPException e) { // hm :/ @@ -402,6 +400,7 @@ public class OpenPgpMessageInputStream extends InputStream { signatures.update(b); } else { in.close(); + collectMetadata(); in = null; try { @@ -426,6 +425,7 @@ public class OpenPgpMessageInputStream extends InputStream { int r = in.read(b, off, len); if (r == -1) { in.close(); + collectMetadata(); in = null; try { @@ -447,6 +447,7 @@ public class OpenPgpMessageInputStream extends InputStream { if (in != null) { in.close(); + collectMetadata(); in = null; } @@ -461,6 +462,21 @@ public class OpenPgpMessageInputStream extends InputStream { closed = true; } + private void collectMetadata() { + if (in instanceof OpenPgpMessageInputStream) { + OpenPgpMessageInputStream child = (OpenPgpMessageInputStream) in; + MessageMetadata.Layer childLayer = child.metadata; + this.metadata.setChild((MessageMetadata.Nested) childLayer); + } + } + + public MessageMetadata getMetadata() { + if (!closed) { + throw new IllegalStateException("Stream must be closed before access to metadata can be granted."); + } + return new MessageMetadata((MessageMetadata.Message) metadata); + } + private static class SortedESKs { private List skesks = new ArrayList<>(); diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/automaton/PDA.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/automaton/PDA.java index 793a3451..feb759ea 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/automaton/PDA.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/automaton/PDA.java @@ -187,10 +187,7 @@ public class PDA { } public void next(InputAlphabet input) throws MalformedOpenPgpMessageException { - State old = state; - StackAlphabet stackItem = stack.isEmpty() ? null : stack.peek(); state = state.transition(input, this); - System.out.println(id + ": Transition from " + old + " to " + state + " via " + input + " with stack " + stackItem); } /** diff --git a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/MessageMetadataTest.java b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/MessageMetadataTest.java new file mode 100644 index 00000000..9f887eb9 --- /dev/null +++ b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/MessageMetadataTest.java @@ -0,0 +1,84 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.decryption_verification; + +import org.junit.JUtils; +import org.junit.jupiter.api.Test; +import org.pgpainless.algorithm.CompressionAlgorithm; +import org.pgpainless.algorithm.StreamEncoding; +import org.pgpainless.algorithm.SymmetricKeyAlgorithm; +import org.pgpainless.util.DateUtil; + +import java.util.Date; +import java.util.Iterator; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class MessageMetadataTest { + + @Test + public void processTestMessage_COMP_ENC_ENC_LIT() { + // Note: COMP of ENC does not make sense, since ENC is indistinguishable from randomness + // and randomness cannot be encrypted. + // For the sake of testing though, this is okay. + MessageMetadata.Message message = new MessageMetadata.Message(); + + MessageMetadata.CompressedData compressedData = new MessageMetadata.CompressedData(CompressionAlgorithm.ZIP); + MessageMetadata.EncryptedData encryptedData = new MessageMetadata.EncryptedData(SymmetricKeyAlgorithm.AES_128); + MessageMetadata.EncryptedData encryptedData1 = new MessageMetadata.EncryptedData(SymmetricKeyAlgorithm.AES_256); + MessageMetadata.LiteralData literalData = new MessageMetadata.LiteralData(); + + message.setChild(compressedData); + compressedData.setChild(encryptedData); + encryptedData.setChild(encryptedData1); + encryptedData1.setChild(literalData); + + MessageMetadata metadata = new MessageMetadata(message); + + // Check encryption algs + assertEquals(SymmetricKeyAlgorithm.AES_128, metadata.getEncryptionAlgorithm(), "getEncryptionAlgorithm() returns alg of outermost EncryptedData"); + Iterator encryptionAlgs = metadata.getEncryptionAlgorithms(); + assertTrue(encryptionAlgs.hasNext(), "There is at least one EncryptedData child"); + assertTrue(encryptionAlgs.hasNext(), "The child is still there"); + assertEquals(SymmetricKeyAlgorithm.AES_128, encryptionAlgs.next(), "The first algo is AES128"); + assertTrue(encryptionAlgs.hasNext(), "There is another EncryptedData"); + assertTrue(encryptionAlgs.hasNext(), "There is *still* another EncryptedData"); + assertEquals(SymmetricKeyAlgorithm.AES_256, encryptionAlgs.next(), "The second algo is AES256"); + assertFalse(encryptionAlgs.hasNext(), "There is no more EncryptedData"); + assertFalse(encryptionAlgs.hasNext(), "There *still* is no more EncryptedData"); + + assertEquals(CompressionAlgorithm.ZIP, metadata.getCompressionAlgorithm(), "getCompressionAlgorithm() returns alg of outermost CompressedData"); + Iterator compAlgs = metadata.getCompressionAlgorithms(); + assertTrue(compAlgs.hasNext()); + assertTrue(compAlgs.hasNext()); + assertEquals(CompressionAlgorithm.ZIP, compAlgs.next()); + assertFalse(compAlgs.hasNext()); + assertFalse(compAlgs.hasNext()); + + assertEquals("", metadata.getFilename()); + JUtils.assertDateEquals(new Date(0L), metadata.getModificationDate()); + assertEquals(StreamEncoding.BINARY, metadata.getFormat()); + } + + @Test + public void testProcessLiteralDataMessage() { + MessageMetadata.LiteralData literalData = new MessageMetadata.LiteralData( + "collateral_murder.zip", + DateUtil.parseUTCDate("2010-04-05 10:12:03 UTC"), + StreamEncoding.BINARY); + MessageMetadata.Message message = new MessageMetadata.Message(); + message.setChild(literalData); + + MessageMetadata metadata = new MessageMetadata(message); + assertNull(metadata.getCompressionAlgorithm()); + assertNull(metadata.getEncryptionAlgorithm()); + assertEquals("collateral_murder.zip", metadata.getFilename()); + assertEquals(DateUtil.parseUTCDate("2010-04-05 10:12:03 UTC"), metadata.getModificationDate()); + assertEquals(StreamEncoding.BINARY, metadata.getFormat()); + } +} diff --git a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStreamTest.java b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStreamTest.java index af1fac44..8219ec68 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStreamTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStreamTest.java @@ -1,5 +1,21 @@ package org.pgpainless.decryption_verification; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.nio.charset.StandardCharsets; +import java.security.InvalidAlgorithmParameterException; +import java.security.NoSuchAlgorithmException; +import java.util.Date; +import java.util.Iterator; +import java.util.stream.Stream; + import org.bouncycastle.bcpg.ArmoredInputStream; import org.bouncycastle.bcpg.ArmoredOutputStream; import org.bouncycastle.bcpg.CompressionAlgorithmTags; @@ -11,9 +27,14 @@ import org.bouncycastle.openpgp.PGPPublicKeyRing; import org.bouncycastle.openpgp.PGPSecretKeyRing; import org.bouncycastle.openpgp.PGPSignature; import org.bouncycastle.util.io.Streams; -import org.junit.jupiter.api.Test; +import org.junit.JUtils; +import org.junit.jupiter.api.Named; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; import org.pgpainless.PGPainless; import org.pgpainless.algorithm.CompressionAlgorithm; +import org.pgpainless.algorithm.StreamEncoding; import org.pgpainless.encryption_signing.EncryptionOptions; import org.pgpainless.encryption_signing.EncryptionResult; import org.pgpainless.encryption_signing.EncryptionStream; @@ -23,17 +44,7 @@ import org.pgpainless.exception.MalformedOpenPgpMessageException; import org.pgpainless.key.protection.SecretKeyRingProtector; import org.pgpainless.util.ArmoredInputStreamFactory; import org.pgpainless.util.Passphrase; - -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.OutputStream; -import java.nio.charset.StandardCharsets; -import java.security.InvalidAlgorithmParameterException; -import java.security.NoSuchAlgorithmException; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertThrows; +import org.pgpainless.util.Tuple; public class OpenPgpMessageInputStreamTest { @@ -304,107 +315,119 @@ public class OpenPgpMessageInputStreamTest { System.out.println(out); } - @Test - public void testProcessLIT() throws IOException, PGPException { - String plain = processReadBuffered(LIT, ConsumerOptions.get()); - assertEquals(PLAINTEXT, plain); - - plain = processReadSequential(LIT, ConsumerOptions.get()); - assertEquals(PLAINTEXT, plain); + interface Processor { + Tuple process(String armoredMessage, ConsumerOptions options) throws PGPException, IOException; } - @Test - public void testProcessLIT_LIT_fails() { + private static Stream provideMessageProcessors() { + return Stream.of( + Arguments.of(Named.of("read(buf,off,len)", (Processor) OpenPgpMessageInputStreamTest::processReadBuffered)), + Arguments.of(Named.of("read()", (Processor) OpenPgpMessageInputStreamTest::processReadSequential))); + } + + @ParameterizedTest(name = "Process LIT using {0}") + @MethodSource("provideMessageProcessors") + public void testProcessLIT(Processor processor) throws IOException, PGPException { + Tuple result = processor.process(LIT, ConsumerOptions.get()); + String plain = result.getA(); + assertEquals(PLAINTEXT, plain); + + MessageMetadata metadata = result.getB(); + assertNull(metadata.getCompressionAlgorithm()); + assertNull(metadata.getEncryptionAlgorithm()); + assertEquals("", metadata.getFilename()); + JUtils.assertDateEquals(new Date(0L), metadata.getModificationDate()); + assertEquals(StreamEncoding.BINARY, metadata.getFormat()); + } + + @ParameterizedTest(name = "Process LIT LIT using {0}") + @MethodSource("provideMessageProcessors") + public void testProcessLIT_LIT_fails(Processor processor) { assertThrows(MalformedOpenPgpMessageException.class, - () -> processReadBuffered(LIT_LIT, ConsumerOptions.get())); + () -> processor.process(LIT_LIT, ConsumerOptions.get())); + } + @ParameterizedTest(name = "Process COMP(LIT) using {0}") + @MethodSource("provideMessageProcessors") + public void testProcessCOMP_LIT(Processor processor) throws PGPException, IOException { + Tuple result = processor.process(COMP_LIT, ConsumerOptions.get()); + String plain = result.getA(); + assertEquals(PLAINTEXT, plain); + MessageMetadata metadata = result.getB(); + assertEquals(CompressionAlgorithm.ZIP, metadata.getCompressionAlgorithm()); + } + + @ParameterizedTest(name = "Process COMP using {0}") + @MethodSource("provideMessageProcessors") + public void testProcessCOMP_fails(Processor processor) { assertThrows(MalformedOpenPgpMessageException.class, - () -> processReadSequential(LIT_LIT, ConsumerOptions.get())); + () -> processor.process(COMP, ConsumerOptions.get())); } - @Test - public void testProcessCOMP_LIT() throws PGPException, IOException { - String plain = processReadBuffered(COMP_LIT, ConsumerOptions.get()); - assertEquals(PLAINTEXT, plain); - - plain = processReadSequential(COMP_LIT, ConsumerOptions.get()); + @ParameterizedTest(name = "Process COMP(COMP(LIT)) using {0}") + @MethodSource("provideMessageProcessors") + public void testProcessCOMP_COMP_LIT(Processor processor) throws PGPException, IOException { + Tuple result = processor.process(COMP_COMP_LIT, ConsumerOptions.get()); + String plain = result.getA(); assertEquals(PLAINTEXT, plain); + MessageMetadata metadata = result.getB(); + assertEquals(CompressionAlgorithm.ZIP, metadata.getCompressionAlgorithm()); + Iterator compressionAlgorithms = metadata.getCompressionAlgorithms(); + assertEquals(CompressionAlgorithm.ZIP, compressionAlgorithms.next()); + assertEquals(CompressionAlgorithm.BZIP2, compressionAlgorithms.next()); + assertFalse(compressionAlgorithms.hasNext()); } - @Test - public void testProcessCOMP_fails() { - assertThrows(MalformedOpenPgpMessageException.class, - () -> processReadBuffered(COMP, ConsumerOptions.get())); - - assertThrows(MalformedOpenPgpMessageException.class, - () -> processReadSequential(COMP, ConsumerOptions.get())); - } - - @Test - public void testProcessCOMP_COMP_LIT() throws PGPException, IOException { - String plain = processReadBuffered(COMP_COMP_LIT, ConsumerOptions.get()); - assertEquals(PLAINTEXT, plain); - - plain = processReadSequential(COMP_COMP_LIT, ConsumerOptions.get()); - assertEquals(PLAINTEXT, plain); - } - - @Test - public void testProcessSIG_LIT() throws PGPException, IOException { + @ParameterizedTest(name = "Process SIG LIT using {0}") + @MethodSource("provideMessageProcessors") + public void testProcessSIG_LIT(Processor processor) throws PGPException, IOException { PGPPublicKeyRing cert = PGPainless.extractCertificate( PGPainless.readKeyRing().secretKeyRing(KEY)); - String plain = processReadBuffered(SIG_LIT, ConsumerOptions.get() - .addVerificationCert(cert)); - assertEquals(PLAINTEXT, plain); - - plain = processReadSequential(SIG_LIT, ConsumerOptions.get() + Tuple result = processor.process(SIG_LIT, ConsumerOptions.get() .addVerificationCert(cert)); + String plain = result.getA(); assertEquals(PLAINTEXT, plain); } - @Test - public void testProcessSENC_LIT() throws PGPException, IOException { - String plain = processReadBuffered(SENC_LIT, ConsumerOptions.get().addDecryptionPassphrase(Passphrase.fromPassword(PASSPHRASE))); - assertEquals(PLAINTEXT, plain); - - plain = processReadSequential(SENC_LIT, ConsumerOptions.get().addDecryptionPassphrase(Passphrase.fromPassword(PASSPHRASE))); + @ParameterizedTest(name = "Process SENC(LIT) using {0}") + @MethodSource("provideMessageProcessors") + public void testProcessSENC_LIT(Processor processor) throws PGPException, IOException { + Tuple result = processor.process(SENC_LIT, ConsumerOptions.get().addDecryptionPassphrase(Passphrase.fromPassword(PASSPHRASE))); + String plain = result.getA(); assertEquals(PLAINTEXT, plain); } - @Test - public void testProcessPENC_COMP_LIT() throws IOException, PGPException { + @ParameterizedTest(name = "Process PENC(LIT) using {0}") + @MethodSource("provideMessageProcessors") + public void testProcessPENC_COMP_LIT(Processor processor) throws IOException, PGPException { PGPSecretKeyRing secretKeys = PGPainless.readKeyRing().secretKeyRing(KEY); - String plain = processReadBuffered(PENC_COMP_LIT, ConsumerOptions.get() - .addDecryptionKey(secretKeys)); - assertEquals(PLAINTEXT, plain); - - plain = processReadSequential(PENC_COMP_LIT, ConsumerOptions.get() + Tuple result = processor.process(PENC_COMP_LIT, ConsumerOptions.get() .addDecryptionKey(secretKeys)); + String plain = result.getA(); assertEquals(PLAINTEXT, plain); } - @Test - public void testProcessOPS_LIT_SIG() throws IOException, PGPException { + @ParameterizedTest(name = "Process OPS LIT SIG using {0}") + @MethodSource("provideMessageProcessors") + public void testProcessOPS_LIT_SIG(Processor processor) throws IOException, PGPException { PGPPublicKeyRing cert = PGPainless.extractCertificate(PGPainless.readKeyRing().secretKeyRing(KEY)); - String plain = processReadBuffered(OPS_LIT_SIG, ConsumerOptions.get() - .addVerificationCert(cert)); - assertEquals(PLAINTEXT, plain); - - plain = processReadSequential(OPS_LIT_SIG, ConsumerOptions.get() + Tuple result = processor.process(OPS_LIT_SIG, ConsumerOptions.get() .addVerificationCert(cert)); + String plain = result.getA(); assertEquals(PLAINTEXT, plain); } - private String processReadBuffered(String armoredMessage, ConsumerOptions options) throws PGPException, IOException { + private static Tuple processReadBuffered(String armoredMessage, ConsumerOptions options) throws PGPException, IOException { OpenPgpMessageInputStream in = get(armoredMessage, options); ByteArrayOutputStream out = new ByteArrayOutputStream(); Streams.pipeAll(in, out); in.close(); - return out.toString(); + MessageMetadata metadata = in.getMetadata(); + return new Tuple<>(out.toString(), metadata); } - private String processReadSequential(String armoredMessage, ConsumerOptions options) throws PGPException, IOException { + private static Tuple processReadSequential(String armoredMessage, ConsumerOptions options) throws PGPException, IOException { OpenPgpMessageInputStream in = get(armoredMessage, options); ByteArrayOutputStream out = new ByteArrayOutputStream(); @@ -414,10 +437,11 @@ public class OpenPgpMessageInputStreamTest { } in.close(); - return out.toString(); + MessageMetadata metadata = in.getMetadata(); + return new Tuple<>(out.toString(), metadata); } - private OpenPgpMessageInputStream get(String armored, ConsumerOptions options) throws IOException, PGPException { + private static OpenPgpMessageInputStream get(String armored, ConsumerOptions options) throws IOException, PGPException { ByteArrayInputStream bytesIn = new ByteArrayInputStream(armored.getBytes(StandardCharsets.UTF_8)); ArmoredInputStream armorIn = ArmoredInputStreamFactory.get(bytesIn); OpenPgpMessageInputStream pgpIn = new OpenPgpMessageInputStream(armorIn, options); From e25f6e17124e8500d28dd84413f956e0477ca887 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 27 Sep 2022 16:11:55 +0200 Subject: [PATCH 025/763] Fix checkstyle issues --- .../MessageMetadata.java | 6 +- .../OpenPgpMessageInputStream.java | 23 +++---- .../automaton/InputAlphabet.java | 4 ++ .../automaton/PDA.java | 4 ++ .../automaton/StackAlphabet.java | 4 ++ .../automaton/package-info.java | 8 +++ .../OpenPgpMessageInputStreamTest.java | 60 +++++++++++-------- .../TeeBCPGInputStreamTest.java | 7 ++- 8 files changed, 75 insertions(+), 41 deletions(-) create mode 100644 pgpainless-core/src/main/java/org/pgpainless/decryption_verification/automaton/package-info.java diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/MessageMetadata.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/MessageMetadata.java index 7c09fc8d..cbf251a8 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/MessageMetadata.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/MessageMetadata.java @@ -90,7 +90,7 @@ public class MessageMetadata { return (LiteralData) nested; } - public static abstract class Layer { + public abstract static class Layer { protected final List verifiedSignatures = new ArrayList<>(); protected final List failedSignatures = new ArrayList<>(); protected Nested child; @@ -198,11 +198,11 @@ public class MessageMetadata { } - private static abstract class LayerIterator implements Iterator { + private abstract static class LayerIterator implements Iterator { private Nested current; Layer last = null; - public LayerIterator(Message message) { + LayerIterator(Message message) { super(); this.current = message.child; if (matches(current)) { diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java index 44ebb27e..58699fcd 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java @@ -50,6 +50,8 @@ import org.pgpainless.key.protection.UnlockSecretKey; import org.pgpainless.signature.SignatureUtils; import org.pgpainless.util.Passphrase; import org.pgpainless.util.Tuple; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import java.io.ByteArrayOutputStream; import java.io.IOException; @@ -60,6 +62,8 @@ import java.util.List; public class OpenPgpMessageInputStream extends InputStream { + private static final Logger LOGGER = LoggerFactory.getLogger(OpenPgpMessageInputStream.class); + protected final PDA automaton = new PDA(); protected final ConsumerOptions options; protected final OpenPgpMetadata.Builder resultBuilder; @@ -519,7 +523,7 @@ public class OpenPgpMessageInputStream extends InputStream { private final PGPOnePassSignatureList list; private final List encapsulating; - public PGPOnePassSignatureListWrapper(PGPOnePassSignatureList signatures, List encapsulating) { + PGPOnePassSignatureListWrapper(PGPOnePassSignatureList signatures, List encapsulating) { this.list = signatures; this.encapsulating = encapsulating; } @@ -529,7 +533,7 @@ public class OpenPgpMessageInputStream extends InputStream { } } - private static class Signatures { + private static final class Signatures { final ConsumerOptions options; List detachedSignatures = new ArrayList<>(); List prependedSignatures = new ArrayList<>(); @@ -551,7 +555,6 @@ public class OpenPgpMessageInputStream extends InputStream { } void addPrependedSignatures(PGPSignatureList signatures) { - System.out.println("Adding " + signatures.size() + " prepended Signatures"); for (PGPSignature signature : signatures) { long keyId = SignatureUtils.determineIssuerKeyId(signature); PGPPublicKeyRing certificate = findCertificate(keyId); @@ -561,7 +564,6 @@ public class OpenPgpMessageInputStream extends InputStream { } void addOnePassSignatures(PGPOnePassSignatureListWrapper signatures) { - System.out.println("Adding " + signatures.size() + " OPSs"); for (PGPOnePassSignature ops : signatures.list) { PGPPublicKeyRing certificate = findCertificate(ops.getKeyID()); initialize(ops, certificate); @@ -570,7 +572,6 @@ public class OpenPgpMessageInputStream extends InputStream { } void addOnePassCorrespondingSignatures(PGPSignatureList signatures) { - System.out.println("Adding " + signatures.size() + " Corresponding Signatures"); for (PGPSignature signature : signatures) { correspondingSignatures.add(signature); } @@ -631,9 +632,9 @@ public class OpenPgpMessageInputStream extends InputStream { try { verified = detached.verify(); } catch (PGPException e) { - System.out.println(e.getMessage()); + LOGGER.debug("Cannot verify detached signature.", e); } - System.out.println("Detached Signature by " + Long.toHexString(detached.getKeyID()) + " is " + (verified ? "verified" : "unverified")); + LOGGER.debug("Detached Signature by " + Long.toHexString(detached.getKeyID()) + " is " + (verified ? "verified" : "unverified")); } for (PGPSignature prepended : prependedSignatures) { @@ -641,9 +642,9 @@ public class OpenPgpMessageInputStream extends InputStream { try { verified = prepended.verify(); } catch (PGPException e) { - System.out.println(e.getMessage()); + LOGGER.debug("Cannot verify prepended signature.", e); } - System.out.println("Prepended Signature by " + Long.toHexString(prepended.getKeyID()) + " is " + (verified ? "verified" : "unverified")); + LOGGER.debug("Prepended Signature by " + Long.toHexString(prepended.getKeyID()) + " is " + (verified ? "verified" : "unverified")); } @@ -654,9 +655,9 @@ public class OpenPgpMessageInputStream extends InputStream { try { verified = ops.verify(signature); } catch (PGPException e) { - System.out.println(e.getMessage()); + LOGGER.debug("Cannot verify OPS signature.", e); } - System.out.println("One-Pass-Signature by " + Long.toHexString(ops.getKeyID()) + " is " + (verified ? "verified" : "unverified")); + LOGGER.debug("One-Pass-Signature by " + Long.toHexString(ops.getKeyID()) + " is " + (verified ? "verified" : "unverified")); } } } diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/automaton/InputAlphabet.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/automaton/InputAlphabet.java index d015a4b3..8e795f5b 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/automaton/InputAlphabet.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/automaton/InputAlphabet.java @@ -1,3 +1,7 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + package org.pgpainless.decryption_verification.automaton; import org.bouncycastle.openpgp.PGPCompressedData; diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/automaton/PDA.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/automaton/PDA.java index feb759ea..dda2adce 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/automaton/PDA.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/automaton/PDA.java @@ -1,3 +1,7 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + package org.pgpainless.decryption_verification.automaton; import org.pgpainless.exception.MalformedOpenPgpMessageException; diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/automaton/StackAlphabet.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/automaton/StackAlphabet.java index 97dad3d8..09865f31 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/automaton/StackAlphabet.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/automaton/StackAlphabet.java @@ -1,3 +1,7 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + package org.pgpainless.decryption_verification.automaton; public enum StackAlphabet { diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/automaton/package-info.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/automaton/package-info.java new file mode 100644 index 00000000..80a79e85 --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/automaton/package-info.java @@ -0,0 +1,8 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +/** + * Pushdown Automaton to verify validity of packet sequences according to the OpenPGP Message format. + */ +package org.pgpainless.decryption_verification.automaton; diff --git a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStreamTest.java b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStreamTest.java index 8219ec68..6962e279 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStreamTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStreamTest.java @@ -35,6 +35,7 @@ import org.junit.jupiter.params.provider.MethodSource; import org.pgpainless.PGPainless; import org.pgpainless.algorithm.CompressionAlgorithm; import org.pgpainless.algorithm.StreamEncoding; +import org.pgpainless.algorithm.SymmetricKeyAlgorithm; import org.pgpainless.encryption_signing.EncryptionOptions; import org.pgpainless.encryption_signing.EncryptionResult; import org.pgpainless.encryption_signing.EncryptionStream; @@ -118,7 +119,7 @@ public class OpenPgpMessageInputStreamTest { "=K9Zl\n" + "-----END PGP MESSAGE-----"; - public static final String SIG_LIT = "" + + public static final String SIG_COMP_LIT = "" + "-----BEGIN PGP MESSAGE-----\n" + "Version: BCPG v1.71\n" + "\n" + @@ -235,9 +236,9 @@ public class OpenPgpMessageInputStreamTest { } public static void genKey() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, IOException { - System.out.println(PGPainless.asciiArmor( - PGPainless.generateKeyRing().modernKeyRing("Alice ") - )); + PGPainless.asciiArmor( + PGPainless.generateKeyRing().modernKeyRing("Alice "), + System.out); } public static void genSIG_LIT() throws PGPException, IOException { @@ -265,54 +266,46 @@ public class OpenPgpMessageInputStreamTest { armorOut.close(); String armored = out.toString(); + // CHECKSTYLE:OFF System.out.println(armored .replace("-----BEGIN PGP SIGNATURE-----\n", "-----BEGIN PGP MESSAGE-----\n") .replace("-----END PGP SIGNATURE-----", "-----END PGP MESSAGE-----")); + // CHECKSTYLE:ON } public static void genSENC_LIT() throws PGPException, IOException { - ByteArrayOutputStream out = new ByteArrayOutputStream(); EncryptionStream enc = PGPainless.encryptAndOrSign() - .onOutputStream(out) + .onOutputStream(System.out) .withOptions(ProducerOptions.encrypt(EncryptionOptions.get() .addPassphrase(Passphrase.fromPassword(PASSPHRASE))) .overrideCompressionAlgorithm(CompressionAlgorithm.UNCOMPRESSED)); enc.write(PLAINTEXT.getBytes(StandardCharsets.UTF_8)); enc.close(); - - System.out.println(out); } public static void genPENC_COMP_LIT() throws IOException, PGPException { PGPSecretKeyRing secretKeys = PGPainless.readKeyRing().secretKeyRing(KEY); PGPPublicKeyRing cert = PGPainless.extractCertificate(secretKeys); - - ByteArrayOutputStream out = new ByteArrayOutputStream(); EncryptionStream enc = PGPainless.encryptAndOrSign() - .onOutputStream(out) + .onOutputStream(System.out) .withOptions(ProducerOptions.encrypt(EncryptionOptions.get() .addRecipient(cert)) .overrideCompressionAlgorithm(CompressionAlgorithm.ZLIB)); Streams.pipeAll(new ByteArrayInputStream(PLAINTEXT.getBytes(StandardCharsets.UTF_8)), enc); enc.close(); - - System.out.println(out); } public static void genOPS_LIT_SIG() throws PGPException, IOException { PGPSecretKeyRing secretKeys = PGPainless.readKeyRing().secretKeyRing(KEY); - ByteArrayOutputStream out = new ByteArrayOutputStream(); EncryptionStream enc = PGPainless.encryptAndOrSign() - .onOutputStream(out) + .onOutputStream(System.out) .withOptions(ProducerOptions.sign(SigningOptions.get() .addSignature(SecretKeyRingProtector.unprotectedKeys(), secretKeys)) .overrideCompressionAlgorithm(CompressionAlgorithm.UNCOMPRESSED)); Streams.pipeAll(new ByteArrayInputStream(PLAINTEXT.getBytes(StandardCharsets.UTF_8)), enc); enc.close(); - - System.out.println(out); } interface Processor { @@ -322,7 +315,8 @@ public class OpenPgpMessageInputStreamTest { private static Stream provideMessageProcessors() { return Stream.of( Arguments.of(Named.of("read(buf,off,len)", (Processor) OpenPgpMessageInputStreamTest::processReadBuffered)), - Arguments.of(Named.of("read()", (Processor) OpenPgpMessageInputStreamTest::processReadSequential))); + Arguments.of(Named.of("read()", (Processor) OpenPgpMessageInputStreamTest::processReadSequential)) + ); } @ParameterizedTest(name = "Process LIT using {0}") @@ -349,7 +343,8 @@ public class OpenPgpMessageInputStreamTest { @ParameterizedTest(name = "Process COMP(LIT) using {0}") @MethodSource("provideMessageProcessors") - public void testProcessCOMP_LIT(Processor processor) throws PGPException, IOException { + public void testProcessCOMP_LIT(Processor processor) + throws PGPException, IOException { Tuple result = processor.process(COMP_LIT, ConsumerOptions.get()); String plain = result.getA(); assertEquals(PLAINTEXT, plain); @@ -366,7 +361,8 @@ public class OpenPgpMessageInputStreamTest { @ParameterizedTest(name = "Process COMP(COMP(LIT)) using {0}") @MethodSource("provideMessageProcessors") - public void testProcessCOMP_COMP_LIT(Processor processor) throws PGPException, IOException { + public void testProcessCOMP_COMP_LIT(Processor processor) + throws PGPException, IOException { Tuple result = processor.process(COMP_COMP_LIT, ConsumerOptions.get()); String plain = result.getA(); assertEquals(PLAINTEXT, plain); @@ -376,29 +372,37 @@ public class OpenPgpMessageInputStreamTest { assertEquals(CompressionAlgorithm.ZIP, compressionAlgorithms.next()); assertEquals(CompressionAlgorithm.BZIP2, compressionAlgorithms.next()); assertFalse(compressionAlgorithms.hasNext()); + assertNull(metadata.getEncryptionAlgorithm()); } - @ParameterizedTest(name = "Process SIG LIT using {0}") + @ParameterizedTest(name = "Process SIG COMP(LIT) using {0}") @MethodSource("provideMessageProcessors") - public void testProcessSIG_LIT(Processor processor) throws PGPException, IOException { + public void testProcessSIG_COMP_LIT(Processor processor) throws PGPException, IOException { PGPPublicKeyRing cert = PGPainless.extractCertificate( PGPainless.readKeyRing().secretKeyRing(KEY)); - Tuple result = processor.process(SIG_LIT, ConsumerOptions.get() + Tuple result = processor.process(SIG_COMP_LIT, ConsumerOptions.get() .addVerificationCert(cert)); String plain = result.getA(); assertEquals(PLAINTEXT, plain); + MessageMetadata metadata = result.getB(); + assertEquals(CompressionAlgorithm.ZIP, metadata.getCompressionAlgorithm()); + assertNull(metadata.getEncryptionAlgorithm()); } @ParameterizedTest(name = "Process SENC(LIT) using {0}") @MethodSource("provideMessageProcessors") public void testProcessSENC_LIT(Processor processor) throws PGPException, IOException { - Tuple result = processor.process(SENC_LIT, ConsumerOptions.get().addDecryptionPassphrase(Passphrase.fromPassword(PASSPHRASE))); + Tuple result = processor.process(SENC_LIT, ConsumerOptions.get() + .addDecryptionPassphrase(Passphrase.fromPassword(PASSPHRASE))); String plain = result.getA(); assertEquals(PLAINTEXT, plain); + MessageMetadata metadata = result.getB(); + assertNull(metadata.getCompressionAlgorithm()); + assertEquals(SymmetricKeyAlgorithm.AES_256, metadata.getEncryptionAlgorithm()); } - @ParameterizedTest(name = "Process PENC(LIT) using {0}") + @ParameterizedTest(name = "Process PENC(COMP(LIT)) using {0}") @MethodSource("provideMessageProcessors") public void testProcessPENC_COMP_LIT(Processor processor) throws IOException, PGPException { PGPSecretKeyRing secretKeys = PGPainless.readKeyRing().secretKeyRing(KEY); @@ -406,6 +410,9 @@ public class OpenPgpMessageInputStreamTest { .addDecryptionKey(secretKeys)); String plain = result.getA(); assertEquals(PLAINTEXT, plain); + MessageMetadata metadata = result.getB(); + assertEquals(CompressionAlgorithm.ZLIB, metadata.getCompressionAlgorithm()); + assertEquals(SymmetricKeyAlgorithm.AES_256, metadata.getEncryptionAlgorithm()); } @ParameterizedTest(name = "Process OPS LIT SIG using {0}") @@ -416,6 +423,9 @@ public class OpenPgpMessageInputStreamTest { .addVerificationCert(cert)); String plain = result.getA(); assertEquals(PLAINTEXT, plain); + MessageMetadata metadata = result.getB(); + assertNull(metadata.getEncryptionAlgorithm()); + assertNull(metadata.getCompressionAlgorithm()); } private static Tuple processReadBuffered(String armoredMessage, ConsumerOptions options) throws PGPException, IOException { diff --git a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/TeeBCPGInputStreamTest.java b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/TeeBCPGInputStreamTest.java index 765221d3..d31c6009 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/TeeBCPGInputStreamTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/TeeBCPGInputStreamTest.java @@ -9,6 +9,8 @@ import org.bouncycastle.openpgp.PGPException; import org.junit.jupiter.api.Test; import org.pgpainless.algorithm.OpenPgpPacket; import org.pgpainless.util.ArmoredInputStreamFactory; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; @@ -18,6 +20,7 @@ import java.nio.charset.StandardCharsets; public class TeeBCPGInputStreamTest { + private static final Logger LOGGER = LoggerFactory.getLogger(TeeBCPGInputStreamTest.class); private static final String INBAND_SIGNED = "-----BEGIN PGP MESSAGE-----\n" + "Version: PGPainless\n" + "\n" + @@ -48,11 +51,11 @@ public class TeeBCPGInputStreamTest { int tag; while ((tag = nestedTeeIn.nextPacketTag()) != -1) { - System.out.println(OpenPgpPacket.requireFromTag(tag)); + LOGGER.debug(OpenPgpPacket.requireFromTag(tag).toString()); Packet packet = nestedTeeIn.readPacket(); } nestedArmorOut.close(); - System.out.println(nestedOut); + LOGGER.debug(nestedOut.toString()); } } From 45555bf82de7e366a99dcc2ac30cad5092f56601 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 28 Sep 2022 16:55:08 +0200 Subject: [PATCH 026/763] Wip: Work on OPS verification --- .../OpenPgpMessageInputStream.java | 200 ++++++++++++------ 1 file changed, 133 insertions(+), 67 deletions(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java index 58699fcd..df3d6805 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java @@ -56,6 +56,7 @@ import org.slf4j.LoggerFactory; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; +import java.io.OutputStream; import java.util.ArrayList; import java.util.Collection; import java.util.List; @@ -64,15 +65,19 @@ public class OpenPgpMessageInputStream extends InputStream { private static final Logger LOGGER = LoggerFactory.getLogger(OpenPgpMessageInputStream.class); - protected final PDA automaton = new PDA(); + // Options to consume the data protected final ConsumerOptions options; protected final OpenPgpMetadata.Builder resultBuilder; - protected final BCPGInputStream bcpgIn; - protected InputStream in; + // Pushdown Automaton to verify validity of OpenPGP packet sequence in an OpenPGP message + protected final PDA automaton = new PDA(); + // InputStream of OpenPGP packets of the current layer + protected final BCPGInputStream packetInputStream; + // InputStream of a nested data packet + protected InputStream nestedInputStream; private boolean closed = false; - private Signatures signatures; + private final Signatures signatures; private MessageMetadata.Layer metadata; public OpenPgpMessageInputStream(InputStream inputStream, ConsumerOptions options) @@ -82,31 +87,45 @@ public class OpenPgpMessageInputStream extends InputStream { OpenPgpMessageInputStream(InputStream inputStream, ConsumerOptions options, MessageMetadata.Layer metadata) throws PGPException, IOException { - // TODO: Use BCPGInputStream.wrap(inputStream); - if (inputStream instanceof BCPGInputStream) { - this.bcpgIn = (BCPGInputStream) inputStream; - } else { - this.bcpgIn = new BCPGInputStream(inputStream); - } this.options = options; this.metadata = metadata; this.resultBuilder = OpenPgpMetadata.getBuilder(); this.signatures = new Signatures(options); - this.signatures.addDetachedSignatures(options.getDetachedSignatures()); - consumePackets(); // nom nom nom + // Add detached signatures only on the outermost OpenPgpMessageInputStream + if (metadata instanceof MessageMetadata.Message) { + this.signatures.addDetachedSignatures(options.getDetachedSignatures()); + } + + // TODO: Use BCPGInputStream.wrap(inputStream); + BCPGInputStream bcpg = null; + if (inputStream instanceof BCPGInputStream) { + bcpg = (BCPGInputStream) inputStream; + } else { + bcpg = new BCPGInputStream(inputStream); + } + this.packetInputStream = new TeeBCPGInputStream(bcpg, signatures); + + // *omnomnom* + consumePackets(); } /** - * This method consumes OpenPGP packets from the current {@link BCPGInputStream}. - * Once it reaches a "nested" OpenPGP packet (Literal Data, Compressed Data, Encrypted Data), it sets
in
- * to the nested stream and breaks the loop. + * Consume OpenPGP packets from the current {@link BCPGInputStream}. + * Once an OpenPGP packet with nested data (Literal Data, Compressed Data, Encrypted Data) is reached, + * set
nestedInputStream
to the nested stream and breaks the loop. * The nested stream is either a simple {@link InputStream} (in case of Literal Data), or another * {@link OpenPgpMessageInputStream} in case of Compressed and Encrypted Data. + * Once the nested data is processed, this method is called again to consume the remainder + * of packets following the nested data packet. * - * @throws IOException - * @throws PGPException + * @throws IOException in case of an IO error + * @throws PGPException in case of an OpenPGP error + * @throws MissingDecryptionMethodException if there is an encrypted data packet which cannot be decrypted + * due to missing decryption methods (no key, no password, no sessionkey) + * @throws MalformedOpenPgpMessageException if the message is made of an invalid packet sequence which + * does not follow the packet syntax of RFC4880. */ private void consumePackets() throws IOException, PGPException { @@ -127,17 +146,23 @@ public class OpenPgpMessageInputStream extends InputStream { processCompressedData(); break loop; - // One Pass Signatures + // One Pass Signature case OPS: automaton.next(InputAlphabet.OnePassSignatures); - signatures.addOnePassSignatures(readOnePassSignatures()); + signatures.addOnePassSignature(readOnePassSignature()); + // signatures.addOnePassSignatures(readOnePassSignatures()); break; - // Signatures - either prepended to the message, or corresponding to the One Pass Signatures + // Signature - either prepended to the message, or corresponding to a One Pass Signature case SIG: boolean isSigForOPS = automaton.peekStack() == StackAlphabet.ops; automaton.next(InputAlphabet.Signatures); - processSignature(isSigForOPS); + PGPSignature signature = readSignature(); + processSignature(signature, isSigForOPS); + /* + PGPSignatureList signatureList = readSignatures(); + processSignatures(signatureList, isSigForOPS); + */ break; // Encrypted Data (ESKs and SED/SEIPD are parsed the same by BC) @@ -154,7 +179,7 @@ public class OpenPgpMessageInputStream extends InputStream { // Marker Packets need to be skipped and ignored case MARKER: - bcpgIn.readPacket(); // skip + packetInputStream.readPacket(); // skip break; // Key Packets are illegal in this context @@ -182,8 +207,15 @@ public class OpenPgpMessageInputStream extends InputStream { } } - private void processSignature(boolean isSigForOPS) throws IOException { - PGPSignatureList signatureList = readSignatures(); + private void processSignature(PGPSignature signature, boolean isSigForOPS) { + if (isSigForOPS) { + signatures.addOnePassCorrespondingSignature(signature); + } else { + signatures.addPrependedSignature(signature); + } + } + + private void processSignatures(PGPSignatureList signatureList, boolean isSigForOPS) throws IOException { if (isSigForOPS) { signatures.addOnePassCorrespondingSignatures(signatureList); } else { @@ -192,21 +224,21 @@ public class OpenPgpMessageInputStream extends InputStream { } private void processCompressedData() throws IOException, PGPException { - PGPCompressedData compressedData = new PGPCompressedData(bcpgIn); + PGPCompressedData compressedData = new PGPCompressedData(packetInputStream); MessageMetadata.CompressedData compressionLayer = new MessageMetadata.CompressedData( CompressionAlgorithm.fromId(compressedData.getAlgorithm())); - in = new OpenPgpMessageInputStream(compressedData.getDataStream(), options, compressionLayer); + nestedInputStream = new OpenPgpMessageInputStream(compressedData.getDataStream(), options, compressionLayer); } private void processLiteralData() throws IOException { - PGPLiteralData literalData = new PGPLiteralData(bcpgIn); + PGPLiteralData literalData = new PGPLiteralData(packetInputStream); this.metadata.setChild(new MessageMetadata.LiteralData(literalData.getFileName(), literalData.getModificationTime(), StreamEncoding.requireFromCode(literalData.getFormat()))); - in = literalData.getDataStream(); + nestedInputStream = literalData.getDataStream(); } private boolean processEncryptedData() throws IOException, PGPException { - PGPEncryptedDataList encDataList = new PGPEncryptedDataList(bcpgIn); + PGPEncryptedDataList encDataList = new PGPEncryptedDataList(packetInputStream); // TODO: Replace with !encDataList.isIntegrityProtected() if (!encDataList.get(0).isIntegrityProtected()) { @@ -225,11 +257,11 @@ public class OpenPgpMessageInputStream extends InputStream { MessageMetadata.EncryptedData encryptedData = new MessageMetadata.EncryptedData(options.getSessionKey().getAlgorithm()); if (esk instanceof PGPPBEEncryptedData) { PGPPBEEncryptedData skesk = (PGPPBEEncryptedData) esk; - in = new OpenPgpMessageInputStream(skesk.getDataStream(decryptorFactory), options, encryptedData); + nestedInputStream = new OpenPgpMessageInputStream(skesk.getDataStream(decryptorFactory), options, encryptedData); return true; } else if (esk instanceof PGPPublicKeyEncryptedData) { PGPPublicKeyEncryptedData pkesk = (PGPPublicKeyEncryptedData) esk; - in = new OpenPgpMessageInputStream(pkesk.getDataStream(decryptorFactory), options, encryptedData); + nestedInputStream = new OpenPgpMessageInputStream(pkesk.getDataStream(decryptorFactory), options, encryptedData); return true; } else { throw new RuntimeException("Unknown ESK class type: " + esk.getClass().getName()); @@ -248,7 +280,7 @@ public class OpenPgpMessageInputStream extends InputStream { InputStream decrypted = skesk.getDataStream(decryptorFactory); MessageMetadata.EncryptedData encryptedData = new MessageMetadata.EncryptedData( SymmetricKeyAlgorithm.requireFromId(skesk.getSymmetricAlgorithm(decryptorFactory))); - in = new OpenPgpMessageInputStream(decrypted, options, encryptedData); + nestedInputStream = new OpenPgpMessageInputStream(decrypted, options, encryptedData); return true; } catch (PGPException e) { // password mismatch? Try next password @@ -274,7 +306,7 @@ public class OpenPgpMessageInputStream extends InputStream { InputStream decrypted = pkesk.getDataStream(decryptorFactory); MessageMetadata.EncryptedData encryptedData = new MessageMetadata.EncryptedData( SymmetricKeyAlgorithm.requireFromId(pkesk.getSymmetricAlgorithm(decryptorFactory))); - in = new OpenPgpMessageInputStream(decrypted, options, encryptedData); + nestedInputStream = new OpenPgpMessageInputStream(decrypted, options, encryptedData); return true; } catch (PGPException e) { // hm :/ @@ -293,7 +325,7 @@ public class OpenPgpMessageInputStream extends InputStream { InputStream decrypted = pkesk.getDataStream(decryptorFactory); MessageMetadata.EncryptedData encryptedData = new MessageMetadata.EncryptedData( SymmetricKeyAlgorithm.requireFromId(pkesk.getSymmetricAlgorithm(decryptorFactory))); - in = new OpenPgpMessageInputStream(decrypted, options, encryptedData); + nestedInputStream = new OpenPgpMessageInputStream(decrypted, options, encryptedData); return true; } catch (PGPException e) { // hm :/ @@ -307,7 +339,7 @@ public class OpenPgpMessageInputStream extends InputStream { private int nextTag() throws IOException { try { - return bcpgIn.nextPacketTag(); + return packetInputStream.nextPacketTag(); } catch (IOException e) { if ("Stream closed".equals(e.getMessage())) { // ZipInflater Streams sometimes close under our feet -.- @@ -345,13 +377,23 @@ public class OpenPgpMessageInputStream extends InputStream { return null; } + private PGPOnePassSignature readOnePassSignature() + throws PGPException, IOException { + return new PGPOnePassSignature(packetInputStream); + } + + private PGPSignature readSignature() + throws PGPException, IOException { + return new PGPSignature(packetInputStream); + } + private PGPOnePassSignatureListWrapper readOnePassSignatures() throws IOException { List encapsulating = new ArrayList<>(); ByteArrayOutputStream buf = new ByteArrayOutputStream(); BCPGOutputStream bcpgOut = new BCPGOutputStream(buf); int tag; while ((tag = nextTag()) == PacketTags.ONE_PASS_SIGNATURE || tag == PacketTags.MARKER) { - Packet packet = bcpgIn.readPacket(); + Packet packet = packetInputStream.readPacket(); if (tag == PacketTags.ONE_PASS_SIGNATURE) { OnePassSignaturePacket sigPacket = (OnePassSignaturePacket) packet; byte[] bytes = sigPacket.getEncoded(); @@ -371,7 +413,7 @@ public class OpenPgpMessageInputStream extends InputStream { BCPGOutputStream bcpgOut = new BCPGOutputStream(buf); int tag = nextTag(); while (tag == PacketTags.SIGNATURE || tag == PacketTags.MARKER) { - Packet packet = bcpgIn.readPacket(); + Packet packet = packetInputStream.readPacket(); if (tag == PacketTags.SIGNATURE) { SignaturePacket sigPacket = (SignaturePacket) packet; sigPacket.encode(bcpgOut); @@ -387,14 +429,14 @@ public class OpenPgpMessageInputStream extends InputStream { @Override public int read() throws IOException { - if (in == null) { + if (nestedInputStream == null) { automaton.assertValid(); return -1; } int r; try { - r = in.read(); + r = nestedInputStream.read(); } catch (IOException e) { r = -1; } @@ -403,9 +445,9 @@ public class OpenPgpMessageInputStream extends InputStream { byte b = (byte) r; signatures.update(b); } else { - in.close(); + nestedInputStream.close(); collectMetadata(); - in = null; + nestedInputStream = null; try { consumePackets(); @@ -421,16 +463,16 @@ public class OpenPgpMessageInputStream extends InputStream { public int read(byte[] b, int off, int len) throws IOException { - if (in == null) { + if (nestedInputStream == null) { automaton.assertValid(); return -1; } - int r = in.read(b, off, len); + int r = nestedInputStream.read(b, off, len); if (r == -1) { - in.close(); + nestedInputStream.close(); collectMetadata(); - in = null; + nestedInputStream = null; try { consumePackets(); @@ -449,10 +491,10 @@ public class OpenPgpMessageInputStream extends InputStream { return; } - if (in != null) { - in.close(); + if (nestedInputStream != null) { + nestedInputStream.close(); collectMetadata(); - in = null; + nestedInputStream = null; } try { @@ -467,8 +509,8 @@ public class OpenPgpMessageInputStream extends InputStream { } private void collectMetadata() { - if (in instanceof OpenPgpMessageInputStream) { - OpenPgpMessageInputStream child = (OpenPgpMessageInputStream) in; + if (nestedInputStream instanceof OpenPgpMessageInputStream) { + OpenPgpMessageInputStream child = (OpenPgpMessageInputStream) nestedInputStream; MessageMetadata.Layer childLayer = child.metadata; this.metadata.setChild((MessageMetadata.Nested) childLayer); } @@ -533,13 +575,14 @@ public class OpenPgpMessageInputStream extends InputStream { } } - private static final class Signatures { + private static final class Signatures extends OutputStream { final ConsumerOptions options; List detachedSignatures = new ArrayList<>(); List prependedSignatures = new ArrayList<>(); - List onePassSignatures = new ArrayList<>(); + List> onePassSignatures = new ArrayList<>(); List correspondingSignatures = new ArrayList<>(); + boolean lastOpsIsContaining = true; private Signatures(ConsumerOptions options) { this.options = options; @@ -547,28 +590,44 @@ public class OpenPgpMessageInputStream extends InputStream { void addDetachedSignatures(Collection signatures) { for (PGPSignature signature : signatures) { - long keyId = SignatureUtils.determineIssuerKeyId(signature); - PGPPublicKeyRing certificate = findCertificate(keyId); - initialize(signature, certificate, keyId); + addDetachedSignature(signature); } - this.detachedSignatures.addAll(signatures); + } + + void addDetachedSignature(PGPSignature signature) { + long keyId = SignatureUtils.determineIssuerKeyId(signature); + PGPPublicKeyRing certificate = findCertificate(keyId); + initialize(signature, certificate, keyId); + this.detachedSignatures.add(signature); } void addPrependedSignatures(PGPSignatureList signatures) { for (PGPSignature signature : signatures) { - long keyId = SignatureUtils.determineIssuerKeyId(signature); - PGPPublicKeyRing certificate = findCertificate(keyId); - initialize(signature, certificate, keyId); - this.prependedSignatures.add(signature); + addPrependedSignature(signature); } } - void addOnePassSignatures(PGPOnePassSignatureListWrapper signatures) { - for (PGPOnePassSignature ops : signatures.list) { - PGPPublicKeyRing certificate = findCertificate(ops.getKeyID()); - initialize(ops, certificate); - this.onePassSignatures.add(ops); + void addPrependedSignature(PGPSignature signature) { + long keyId = SignatureUtils.determineIssuerKeyId(signature); + PGPPublicKeyRing certificate = findCertificate(keyId); + initialize(signature, certificate, keyId); + this.prependedSignatures.add(signature); + } + + void addOnePassSignature(PGPOnePassSignature signature) { + List list; + if (lastOpsIsContaining) { + list = new ArrayList<>(); + onePassSignatures.add(list); + } else { + list = onePassSignatures.get(onePassSignatures.size() - 1); } + + PGPPublicKeyRing certificate = findCertificate(signature.getKeyID()); + initialize(signature, certificate); + list.add(signature); + + // lastOpsIsContaining = signature.isContaining(); } void addOnePassCorrespondingSignatures(PGPSignatureList signatures) { @@ -618,8 +677,10 @@ public class OpenPgpMessageInputStream extends InputStream { for (PGPSignature prepended : prependedSignatures) { prepended.update(b); } - for (PGPOnePassSignature ops : onePassSignatures) { - ops.update(b); + for (List opss : onePassSignatures) { + for (PGPOnePassSignature ops : opss) { + ops.update(b); + } } for (PGPSignature detached : detachedSignatures) { detached.update(b); @@ -660,5 +721,10 @@ public class OpenPgpMessageInputStream extends InputStream { LOGGER.debug("One-Pass-Signature by " + Long.toHexString(ops.getKeyID()) + " is " + (verified ? "verified" : "unverified")); } } + + @Override + public void write(int b) throws IOException { + update((byte) b); + } } } From 4e44691ef68295f96fdf6ae23456a414fe4f41c8 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 28 Sep 2022 17:38:20 +0200 Subject: [PATCH 027/763] Wip --- .../OpenPgpMessageInputStream.java | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java index df3d6805..36de2635 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java @@ -60,6 +60,7 @@ import java.io.OutputStream; import java.util.ArrayList; import java.util.Collection; import java.util.List; +import java.util.Stack; public class OpenPgpMessageInputStream extends InputStream { @@ -577,15 +578,20 @@ public class OpenPgpMessageInputStream extends InputStream { private static final class Signatures extends OutputStream { final ConsumerOptions options; - List detachedSignatures = new ArrayList<>(); - List prependedSignatures = new ArrayList<>(); - List> onePassSignatures = new ArrayList<>(); - List correspondingSignatures = new ArrayList<>(); + final List detachedSignatures; + final List prependedSignatures; + final Stack> onePassSignatures; + final List correspondingSignatures; boolean lastOpsIsContaining = true; private Signatures(ConsumerOptions options) { this.options = options; + this.detachedSignatures = new ArrayList<>(); + this.prependedSignatures = new ArrayList<>(); + this.onePassSignatures = new Stack<>(); + onePassSignatures.push(new ArrayList<>()); + this.correspondingSignatures = new ArrayList<>(); } void addDetachedSignatures(Collection signatures) { From ec28ba2924667a5ff3719f51c941f57a9275ca98 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 29 Sep 2022 00:15:18 +0200 Subject: [PATCH 028/763] SIGNATURE VERIFICATION IN OPENPGP SUCKS BIG TIME --- .../OpenPgpMessageInputStream.java | 198 ++++++++++-------- .../OpenPgpMessageInputStreamTest.java | 22 +- 2 files changed, 128 insertions(+), 92 deletions(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java index 36de2635..a1364997 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java @@ -48,6 +48,7 @@ import org.pgpainless.key.info.KeyRingInfo; import org.pgpainless.key.protection.SecretKeyRingProtector; import org.pgpainless.key.protection.UnlockSecretKey; import org.pgpainless.signature.SignatureUtils; +import org.pgpainless.util.ArmorUtils; import org.pgpainless.util.Passphrase; import org.pgpainless.util.Tuple; import org.slf4j.Logger; @@ -138,19 +139,25 @@ public class OpenPgpMessageInputStream extends InputStream { // Literal Data - the literal data content is the new input stream case LIT: automaton.next(InputAlphabet.LiteralData); + signatures.commitNested(); processLiteralData(); break loop; // Compressed Data - the content contains another OpenPGP message case COMP: automaton.next(InputAlphabet.CompressedData); + signatures.commitNested(); processCompressedData(); break loop; // One Pass Signature case OPS: automaton.next(InputAlphabet.OnePassSignatures); - signatures.addOnePassSignature(readOnePassSignature()); + // signatures.addOnePassSignature(readOnePassSignature()); + PGPOnePassSignatureList onePassSignatureList = readOnePassSignatures(); + for (PGPOnePassSignature ops : onePassSignatureList) { + signatures.addOnePassSignature(ops); + } // signatures.addOnePassSignatures(readOnePassSignatures()); break; @@ -158,12 +165,15 @@ public class OpenPgpMessageInputStream extends InputStream { case SIG: boolean isSigForOPS = automaton.peekStack() == StackAlphabet.ops; automaton.next(InputAlphabet.Signatures); - PGPSignature signature = readSignature(); - processSignature(signature, isSigForOPS); - /* + + // PGPSignature signature = readSignature(); + // processSignature(signature, isSigForOPS); + PGPSignatureList signatureList = readSignatures(); - processSignatures(signatureList, isSigForOPS); - */ + for (PGPSignature signature : signatureList) { + processSignature(signature, isSigForOPS); + } + break; // Encrypted Data (ESKs and SED/SEIPD are parsed the same by BC) @@ -178,7 +188,7 @@ public class OpenPgpMessageInputStream extends InputStream { throw new MissingDecryptionMethodException("No working decryption method found."); - // Marker Packets need to be skipped and ignored + // Marker Packets need to be skipped and ignored case MARKER: packetInputStream.readPacket(); // skip break; @@ -193,8 +203,8 @@ public class OpenPgpMessageInputStream extends InputStream { case UATTR: throw new MalformedOpenPgpMessageException("Illegal Packet in Stream: " + nextPacket); - // MDC packet is usually processed by PGPEncryptedDataList, so it is very likely we encounter this - // packet out of order + // MDC packet is usually processed by PGPEncryptedDataList, so it is very likely we encounter this + // packet out of order case MDC: throw new MalformedOpenPgpMessageException("Unexpected Packet in Stream: " + nextPacket); @@ -210,20 +220,12 @@ public class OpenPgpMessageInputStream extends InputStream { private void processSignature(PGPSignature signature, boolean isSigForOPS) { if (isSigForOPS) { - signatures.addOnePassCorrespondingSignature(signature); + signatures.addCorrespondingOnePassSignature(signature); } else { signatures.addPrependedSignature(signature); } } - private void processSignatures(PGPSignatureList signatureList, boolean isSigForOPS) throws IOException { - if (isSigForOPS) { - signatures.addOnePassCorrespondingSignatures(signatureList); - } else { - signatures.addPrependedSignatures(signatureList); - } - } - private void processCompressedData() throws IOException, PGPException { PGPCompressedData compressedData = new PGPCompressedData(packetInputStream); MessageMetadata.CompressedData compressionLayer = new MessageMetadata.CompressedData( @@ -380,16 +382,17 @@ public class OpenPgpMessageInputStream extends InputStream { private PGPOnePassSignature readOnePassSignature() throws PGPException, IOException { - return new PGPOnePassSignature(packetInputStream); + //return new PGPOnePassSignature(packetInputStream); + return null; } private PGPSignature readSignature() throws PGPException, IOException { - return new PGPSignature(packetInputStream); + //return new PGPSignature(packetInputStream); + return null; } - private PGPOnePassSignatureListWrapper readOnePassSignatures() throws IOException { - List encapsulating = new ArrayList<>(); + private PGPOnePassSignatureList readOnePassSignatures() throws IOException { ByteArrayOutputStream buf = new ByteArrayOutputStream(); BCPGOutputStream bcpgOut = new BCPGOutputStream(buf); int tag; @@ -398,7 +401,6 @@ public class OpenPgpMessageInputStream extends InputStream { if (tag == PacketTags.ONE_PASS_SIGNATURE) { OnePassSignaturePacket sigPacket = (OnePassSignaturePacket) packet; byte[] bytes = sigPacket.getEncoded(); - encapsulating.add(bytes[bytes.length - 1] == 1); bcpgOut.write(bytes); } } @@ -406,7 +408,7 @@ public class OpenPgpMessageInputStream extends InputStream { PGPObjectFactory objectFactory = ImplementationFactory.getInstance().getPGPObjectFactory(buf.toByteArray()); PGPOnePassSignatureList signatureList = (PGPOnePassSignatureList) objectFactory.nextObject(); - return new PGPOnePassSignatureListWrapper(signatureList, encapsulating); + return signatureList; } private PGPSignatureList readSignatures() throws IOException { @@ -449,6 +451,7 @@ public class OpenPgpMessageInputStream extends InputStream { nestedInputStream.close(); collectMetadata(); nestedInputStream = null; + signatures.popNested(); try { consumePackets(); @@ -474,6 +477,7 @@ public class OpenPgpMessageInputStream extends InputStream { nestedInputStream.close(); collectMetadata(); nestedInputStream = null; + signatures.popNested(); try { consumePackets(); @@ -556,41 +560,29 @@ public class OpenPgpMessageInputStream extends InputStream { } } - /** - * Workaround for BC not exposing, whether an OPS is encapsulating or not. - * TODO: Remove once our PR is merged - * - * @see PR against BC - */ - private static class PGPOnePassSignatureListWrapper { - private final PGPOnePassSignatureList list; - private final List encapsulating; - - PGPOnePassSignatureListWrapper(PGPOnePassSignatureList signatures, List encapsulating) { - this.list = signatures; - this.encapsulating = encapsulating; - } - - public int size() { - return list.size(); - } - } - + // TODO: In 'OPS LIT("Foo") SIG', OPS is only updated with "Foo" + // In 'OPS[1] OPS LIT("Foo") SIG SIG', OPS[1] (nested) is updated with OPS LIT("Foo") SIG. + // Therefore, we need to handle the innermost signature layer differently when updating with Literal data. + // For this we might want to provide two update entries into the Signatures class, one for OpenPGP packets and one + // for literal data. UUUUUGLY!!!! private static final class Signatures extends OutputStream { final ConsumerOptions options; final List detachedSignatures; final List prependedSignatures; - final Stack> onePassSignatures; + final List onePassSignatures; + final Stack> opsUpdateStack; final List correspondingSignatures; - boolean lastOpsIsContaining = true; + ByteArrayOutputStream out = new ByteArrayOutputStream(); + + List opsCurrentNesting = new ArrayList<>(); private Signatures(ConsumerOptions options) { this.options = options; this.detachedSignatures = new ArrayList<>(); this.prependedSignatures = new ArrayList<>(); - this.onePassSignatures = new Stack<>(); - onePassSignatures.push(new ArrayList<>()); + this.onePassSignatures = new ArrayList<>(); + this.opsUpdateStack = new Stack<>(); this.correspondingSignatures = new ArrayList<>(); } @@ -607,12 +599,6 @@ public class OpenPgpMessageInputStream extends InputStream { this.detachedSignatures.add(signature); } - void addPrependedSignatures(PGPSignatureList signatures) { - for (PGPSignature signature : signatures) { - addPrependedSignature(signature); - } - } - void addPrependedSignature(PGPSignature signature) { long keyId = SignatureUtils.determineIssuerKeyId(signature); PGPPublicKeyRing certificate = findCertificate(keyId); @@ -621,27 +607,61 @@ public class OpenPgpMessageInputStream extends InputStream { } void addOnePassSignature(PGPOnePassSignature signature) { - List list; - if (lastOpsIsContaining) { - list = new ArrayList<>(); - onePassSignatures.add(list); - } else { - list = onePassSignatures.get(onePassSignatures.size() - 1); - } - PGPPublicKeyRing certificate = findCertificate(signature.getKeyID()); initialize(signature, certificate); - list.add(signature); + onePassSignatures.add(signature); - // lastOpsIsContaining = signature.isContaining(); + opsCurrentNesting.add(signature); + if (isContaining(signature)) { + commitNested(); + } } - void addOnePassCorrespondingSignatures(PGPSignatureList signatures) { - for (PGPSignature signature : signatures) { - correspondingSignatures.add(signature); + boolean isContaining(PGPOnePassSignature ops) { + try { + byte[] bytes = ops.getEncoded(); + return bytes[bytes.length - 1] == 1; + } catch (IOException e) { + return false; } } + void addCorrespondingOnePassSignature(PGPSignature signature) { + for (PGPOnePassSignature onePassSignature : onePassSignatures) { + if (onePassSignature.getKeyID() != signature.getKeyID()) { + continue; + } + + boolean verified = false; + try { + verified = onePassSignature.verify(signature); + } catch (PGPException e) { + log("Cannot verify OPS signature.", e); + } + log("One-Pass-Signature by " + Long.toHexString(onePassSignature.getKeyID()) + " is " + (verified ? "verified" : "unverified")); + try { + log(ArmorUtils.toAsciiArmoredString(out.toByteArray())); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + } + + void commitNested() { + if (opsCurrentNesting.isEmpty()) { + return; + } + + log("Committing " + opsCurrentNesting.size() + " OPS sigs for updating"); + opsUpdateStack.push(opsCurrentNesting); + opsCurrentNesting = new ArrayList<>(); + } + + void popNested() { + log("Popping nested"); + opsUpdateStack.pop(); + } + private void initialize(PGPSignature signature, PGPPublicKeyRing certificate, long keyId) { if (certificate == null) { // SHIT @@ -680,14 +700,21 @@ public class OpenPgpMessageInputStream extends InputStream { } public void update(byte b) { + if (!opsUpdateStack.isEmpty()) { + log("Update"); + out.write(b); + } + for (PGPSignature prepended : prependedSignatures) { prepended.update(b); } - for (List opss : onePassSignatures) { + + for (List opss : opsUpdateStack) { for (PGPOnePassSignature ops : opss) { ops.update(b); } } + for (PGPSignature detached : detachedSignatures) { detached.update(b); } @@ -699,9 +726,9 @@ public class OpenPgpMessageInputStream extends InputStream { try { verified = detached.verify(); } catch (PGPException e) { - LOGGER.debug("Cannot verify detached signature.", e); + log("Cannot verify detached signature.", e); } - LOGGER.debug("Detached Signature by " + Long.toHexString(detached.getKeyID()) + " is " + (verified ? "verified" : "unverified")); + log("Detached Signature by " + Long.toHexString(detached.getKeyID()) + " is " + (verified ? "verified" : "unverified")); } for (PGPSignature prepended : prependedSignatures) { @@ -709,22 +736,9 @@ public class OpenPgpMessageInputStream extends InputStream { try { verified = prepended.verify(); } catch (PGPException e) { - LOGGER.debug("Cannot verify prepended signature.", e); + log("Cannot verify prepended signature.", e); } - LOGGER.debug("Prepended Signature by " + Long.toHexString(prepended.getKeyID()) + " is " + (verified ? "verified" : "unverified")); - } - - - for (int i = 0; i < onePassSignatures.size(); i++) { - PGPOnePassSignature ops = onePassSignatures.get(i); - PGPSignature signature = correspondingSignatures.get(correspondingSignatures.size() - i - 1); - boolean verified = false; - try { - verified = ops.verify(signature); - } catch (PGPException e) { - LOGGER.debug("Cannot verify OPS signature.", e); - } - LOGGER.debug("One-Pass-Signature by " + Long.toHexString(ops.getKeyID()) + " is " + (verified ? "verified" : "unverified")); + log("Prepended Signature by " + Long.toHexString(prepended.getKeyID()) + " is " + (verified ? "verified" : "unverified")); } } @@ -733,4 +747,18 @@ public class OpenPgpMessageInputStream extends InputStream { update((byte) b); } } + + static void log(String message) { + LOGGER.debug(message); + // CHECKSTYLE:OFF + System.out.println(message); + // CHECKSTYLE:ON + } + + static void log(String message, Throwable e) { + log(message); + // CHECKSTYLE:OFF + e.printStackTrace(); + // CHECKSTYLE:ON + } } diff --git a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStreamTest.java b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStreamTest.java index 6962e279..e52a6016 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStreamTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStreamTest.java @@ -4,6 +4,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; @@ -377,7 +378,8 @@ public class OpenPgpMessageInputStreamTest { @ParameterizedTest(name = "Process SIG COMP(LIT) using {0}") @MethodSource("provideMessageProcessors") - public void testProcessSIG_COMP_LIT(Processor processor) throws PGPException, IOException { + public void testProcessSIG_COMP_LIT(Processor processor) + throws PGPException, IOException { PGPPublicKeyRing cert = PGPainless.extractCertificate( PGPainless.readKeyRing().secretKeyRing(KEY)); @@ -392,7 +394,8 @@ public class OpenPgpMessageInputStreamTest { @ParameterizedTest(name = "Process SENC(LIT) using {0}") @MethodSource("provideMessageProcessors") - public void testProcessSENC_LIT(Processor processor) throws PGPException, IOException { + public void testProcessSENC_LIT(Processor processor) + throws PGPException, IOException { Tuple result = processor.process(SENC_LIT, ConsumerOptions.get() .addDecryptionPassphrase(Passphrase.fromPassword(PASSPHRASE))); String plain = result.getA(); @@ -404,7 +407,8 @@ public class OpenPgpMessageInputStreamTest { @ParameterizedTest(name = "Process PENC(COMP(LIT)) using {0}") @MethodSource("provideMessageProcessors") - public void testProcessPENC_COMP_LIT(Processor processor) throws IOException, PGPException { + public void testProcessPENC_COMP_LIT(Processor processor) + throws IOException, PGPException { PGPSecretKeyRing secretKeys = PGPainless.readKeyRing().secretKeyRing(KEY); Tuple result = processor.process(PENC_COMP_LIT, ConsumerOptions.get() .addDecryptionKey(secretKeys)); @@ -417,7 +421,8 @@ public class OpenPgpMessageInputStreamTest { @ParameterizedTest(name = "Process OPS LIT SIG using {0}") @MethodSource("provideMessageProcessors") - public void testProcessOPS_LIT_SIG(Processor processor) throws IOException, PGPException { + public void testProcessOPS_LIT_SIG(Processor processor) + throws IOException, PGPException { PGPPublicKeyRing cert = PGPainless.extractCertificate(PGPainless.readKeyRing().secretKeyRing(KEY)); Tuple result = processor.process(OPS_LIT_SIG, ConsumerOptions.get() .addVerificationCert(cert)); @@ -428,7 +433,8 @@ public class OpenPgpMessageInputStreamTest { assertNull(metadata.getCompressionAlgorithm()); } - private static Tuple processReadBuffered(String armoredMessage, ConsumerOptions options) throws PGPException, IOException { + private static Tuple processReadBuffered(String armoredMessage, ConsumerOptions options) + throws PGPException, IOException { OpenPgpMessageInputStream in = get(armoredMessage, options); ByteArrayOutputStream out = new ByteArrayOutputStream(); Streams.pipeAll(in, out); @@ -437,7 +443,8 @@ public class OpenPgpMessageInputStreamTest { return new Tuple<>(out.toString(), metadata); } - private static Tuple processReadSequential(String armoredMessage, ConsumerOptions options) throws PGPException, IOException { + private static Tuple processReadSequential(String armoredMessage, ConsumerOptions options) + throws PGPException, IOException { OpenPgpMessageInputStream in = get(armoredMessage, options); ByteArrayOutputStream out = new ByteArrayOutputStream(); @@ -451,7 +458,8 @@ public class OpenPgpMessageInputStreamTest { return new Tuple<>(out.toString(), metadata); } - private static OpenPgpMessageInputStream get(String armored, ConsumerOptions options) throws IOException, PGPException { + private static OpenPgpMessageInputStream get(String armored, ConsumerOptions options) + throws IOException, PGPException { ByteArrayInputStream bytesIn = new ByteArrayInputStream(armored.getBytes(StandardCharsets.UTF_8)); ArmoredInputStream armorIn = ArmoredInputStreamFactory.get(bytesIn); OpenPgpMessageInputStream pgpIn = new OpenPgpMessageInputStream(armorIn, options); From 527aab922ee3f8cd1982a1f6083472e2bd83135f Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 29 Sep 2022 12:33:08 +0200 Subject: [PATCH 029/763] Fix ModificationDetectionException by not calling PGPUtil.getDecoderStream() --- .../DecryptionStreamFactory.java | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) 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 a787280a..07be7c10 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 @@ -36,7 +36,6 @@ import org.bouncycastle.openpgp.PGPSecretKeyRing; import org.bouncycastle.openpgp.PGPSessionKey; import org.bouncycastle.openpgp.PGPSignature; import org.bouncycastle.openpgp.PGPSignatureList; -import org.bouncycastle.openpgp.PGPUtil; import org.bouncycastle.openpgp.operator.PBEDataDecryptorFactory; import org.bouncycastle.openpgp.operator.PGPContentVerifierBuilderProvider; import org.bouncycastle.openpgp.operator.PublicKeyDataDecryptorFactory; @@ -241,14 +240,12 @@ public final class DecryptionStreamFactory { SessionKey sessionKey = options.getSessionKey(); if (sessionKey != null) { integrityProtectedEncryptedInputStream = decryptWithProvidedSessionKey(pgpEncryptedDataList, sessionKey); - InputStream decodedDataStream = PGPUtil.getDecoderStream(integrityProtectedEncryptedInputStream); - PGPObjectFactory factory = ImplementationFactory.getInstance().getPGPObjectFactory(decodedDataStream); + PGPObjectFactory factory = ImplementationFactory.getInstance().getPGPObjectFactory(integrityProtectedEncryptedInputStream); return processPGPPackets(factory, ++depth); } InputStream decryptedDataStream = decryptSessionKey(pgpEncryptedDataList); - InputStream decodedDataStream = PGPUtil.getDecoderStream(decryptedDataStream); - PGPObjectFactory factory = ImplementationFactory.getInstance().getPGPObjectFactory(decodedDataStream); + PGPObjectFactory factory = ImplementationFactory.getInstance().getPGPObjectFactory(decryptedDataStream); return processPGPPackets(factory, ++depth); } @@ -299,8 +296,7 @@ public final class DecryptionStreamFactory { } InputStream inflatedDataStream = pgpCompressedData.getDataStream(); - InputStream decodedDataStream = PGPUtil.getDecoderStream(inflatedDataStream); - PGPObjectFactory objectFactory = ImplementationFactory.getInstance().getPGPObjectFactory(decodedDataStream); + PGPObjectFactory objectFactory = ImplementationFactory.getInstance().getPGPObjectFactory(inflatedDataStream); return processPGPPackets(objectFactory, ++depth); } From 81bb8cba54c5d5040785e8e12829525acbf8ef71 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 29 Sep 2022 12:50:32 +0200 Subject: [PATCH 030/763] Use BCs Arrays.constantTimeAreEqual(char[], char[]) --- .../main/java/org/pgpainless/util/BCUtil.java | 54 ------------------- .../java/org/pgpainless/util/Passphrase.java | 4 +- .../java/org/pgpainless/util/BCUtilTest.java | 17 +++--- 3 files changed, 10 insertions(+), 65 deletions(-) delete mode 100644 pgpainless-core/src/main/java/org/pgpainless/util/BCUtil.java diff --git a/pgpainless-core/src/main/java/org/pgpainless/util/BCUtil.java b/pgpainless-core/src/main/java/org/pgpainless/util/BCUtil.java deleted file mode 100644 index bb811cea..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/util/BCUtil.java +++ /dev/null @@ -1,54 +0,0 @@ -// SPDX-FileCopyrightText: 2018 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.util; - -public final class BCUtil { - - private BCUtil() { - - } - - /** - * A constant time equals comparison - does not terminate early if - * test will fail. For best results always pass the expected value - * as the first parameter. - * - * TODO: This method was proposed as a patch to BC: - * https://github.com/bcgit/bc-java/pull/1141 - * Replace usage of this method with upstream eventually. - * Remove once BC 172 gets released, given it contains the patch. - * - * @param expected first array - * @param supplied second array - * @return true if arrays equal, false otherwise. - */ - public static boolean constantTimeAreEqual( - char[] expected, - char[] supplied) { - if (expected == null || supplied == null) { - return false; - } - - if (expected == supplied) { - return true; - } - - int len = Math.min(expected.length, supplied.length); - - int nonEqual = expected.length ^ supplied.length; - - // do the char-wise comparison - for (int i = 0; i != len; i++) { - nonEqual |= (expected[i] ^ supplied[i]); - } - // If supplied is longer than expected, iterate over rest of supplied with NOPs - for (int i = len; i < supplied.length; i++) { - nonEqual |= ((byte) supplied[i] ^ (byte) ~supplied[i]); - } - - return nonEqual == 0; - } - -} diff --git a/pgpainless-core/src/main/java/org/pgpainless/util/Passphrase.java b/pgpainless-core/src/main/java/org/pgpainless/util/Passphrase.java index 4cef145a..9576fb3e 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/util/Passphrase.java +++ b/pgpainless-core/src/main/java/org/pgpainless/util/Passphrase.java @@ -8,8 +8,6 @@ import javax.annotation.Nonnull; import javax.annotation.Nullable; import java.util.Arrays; -import static org.pgpainless.util.BCUtil.constantTimeAreEqual; - public class Passphrase { public final Object lock = new Object(); @@ -165,6 +163,6 @@ public class Passphrase { } Passphrase other = (Passphrase) obj; return (getChars() == null && other.getChars() == null) || - constantTimeAreEqual(getChars(), other.getChars()); + org.bouncycastle.util.Arrays.constantTimeAreEqual(getChars(), other.getChars()); } } diff --git a/pgpainless-core/src/test/java/org/pgpainless/util/BCUtilTest.java b/pgpainless-core/src/test/java/org/pgpainless/util/BCUtilTest.java index 6fb90e63..82368762 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/util/BCUtilTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/util/BCUtilTest.java @@ -19,6 +19,7 @@ import org.bouncycastle.openpgp.PGPPublicKeyRing; import org.bouncycastle.openpgp.PGPPublicKeyRingCollection; import org.bouncycastle.openpgp.PGPSecretKeyRing; import org.bouncycastle.openpgp.PGPSecretKeyRingCollection; +import org.bouncycastle.util.Arrays; import org.junit.jupiter.api.Test; import org.pgpainless.PGPainless; import org.pgpainless.algorithm.KeyFlag; @@ -94,14 +95,14 @@ public class BCUtilTest { @Test public void constantTimeAreEqualsTest() { char[] b = "Hello".toCharArray(); - assertTrue(BCUtil.constantTimeAreEqual(b, b)); - assertTrue(BCUtil.constantTimeAreEqual("Hello".toCharArray(), "Hello".toCharArray())); - assertTrue(BCUtil.constantTimeAreEqual(new char[0], new char[0])); - assertTrue(BCUtil.constantTimeAreEqual(new char[] {'H', 'e', 'l', 'l', 'o'}, "Hello".toCharArray())); + assertTrue(Arrays.constantTimeAreEqual(b, b)); + assertTrue(Arrays.constantTimeAreEqual("Hello".toCharArray(), "Hello".toCharArray())); + assertTrue(Arrays.constantTimeAreEqual(new char[0], new char[0])); + assertTrue(Arrays.constantTimeAreEqual(new char[] {'H', 'e', 'l', 'l', 'o'}, "Hello".toCharArray())); - assertFalse(BCUtil.constantTimeAreEqual("Hello".toCharArray(), "Hello World".toCharArray())); - assertFalse(BCUtil.constantTimeAreEqual(null, "Hello".toCharArray())); - assertFalse(BCUtil.constantTimeAreEqual("Hello".toCharArray(), null)); - assertFalse(BCUtil.constantTimeAreEqual(null, null)); + assertFalse(Arrays.constantTimeAreEqual("Hello".toCharArray(), "Hello World".toCharArray())); + assertFalse(Arrays.constantTimeAreEqual(null, "Hello".toCharArray())); + assertFalse(Arrays.constantTimeAreEqual("Hello".toCharArray(), null)); + assertFalse(Arrays.constantTimeAreEqual((char[]) null, null)); } } From 09f94944b3f270dd1956733dfef1a7be9a5a14fb Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 29 Sep 2022 13:02:22 +0200 Subject: [PATCH 031/763] Remove unnecessary throws declarations --- .../key/collection/PGPKeyRingCollection.java | 2 +- .../secretkeyring/SecretKeyRingEditor.java | 2 +- .../pgpainless/key/parsing/KeyRingReader.java | 6 ++---- .../org/pgpainless/key/util/KeyRingUtils.java | 17 +++-------------- .../ModificationDetectionTests.java | 2 +- .../EncryptionOptionsTest.java | 3 +-- .../certification/CertifyCertificateTest.java | 2 +- .../GenerateKeyWithAdditionalUserIdTest.java | 6 +++--- .../java/org/pgpainless/util/BCUtilTest.java | 4 +--- .../keyring/KeyRingsFromCollectionTest.java | 4 ++-- .../java/org/pgpainless/sop/DearmorImpl.java | 2 +- .../java/org/pgpainless/sop/InlineSignImpl.java | 2 +- .../org/pgpainless/sop/InlineVerifyImpl.java | 2 +- 13 files changed, 19 insertions(+), 35 deletions(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/collection/PGPKeyRingCollection.java b/pgpainless-core/src/main/java/org/pgpainless/key/collection/PGPKeyRingCollection.java index 46aa2baf..6cf7102d 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/key/collection/PGPKeyRingCollection.java +++ b/pgpainless-core/src/main/java/org/pgpainless/key/collection/PGPKeyRingCollection.java @@ -74,7 +74,7 @@ public class PGPKeyRingCollection { } public PGPKeyRingCollection(@Nonnull Collection collection, boolean isSilent) - throws IOException, PGPException { + throws PGPException { List secretKeyRings = new ArrayList<>(); List publicKeyRings = new ArrayList<>(); 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 20876c36..01c09903 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 @@ -327,7 +327,7 @@ public class SecretKeyRingEditor implements SecretKeyRingEditorInterface { @Nonnull SecretKeyRingProtector primaryKeyProtector, @Nonnull KeyFlag keyFlag, KeyFlag... additionalKeyFlags) - throws PGPException, IOException, NoSuchAlgorithmException { + throws PGPException, IOException { KeyFlag[] flags = concat(keyFlag, additionalKeyFlags); PublicKeyAlgorithm subkeyAlgorithm = PublicKeyAlgorithm.requireFromId(subkey.getPublicKey().getAlgorithm()); SignatureSubpacketsUtil.assureKeyCanCarryFlags(subkeyAlgorithm); 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 b5b8ea65..bbaecd4f 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 @@ -232,10 +232,9 @@ public class KeyRingReader { * @return public key ring collection * * @throws IOException in case of an IO error or exceeding of max iterations - * @throws PGPException in case of a broken key */ public static PGPPublicKeyRingCollection readPublicKeyRingCollection(@Nonnull InputStream inputStream, int maxIterations) - throws IOException, PGPException { + throws IOException { PGPObjectFactory objectFactory = ImplementationFactory.getInstance().getPGPObjectFactory( ArmorUtils.getDecoderStream(inputStream)); @@ -317,11 +316,10 @@ public class KeyRingReader { * @return secret key ring collection * * @throws IOException in case of an IO error or exceeding of max iterations - * @throws PGPException in case of a broken secret key */ public static PGPSecretKeyRingCollection readSecretKeyRingCollection(@Nonnull InputStream inputStream, int maxIterations) - throws IOException, PGPException { + throws IOException { PGPObjectFactory objectFactory = ImplementationFactory.getInstance().getPGPObjectFactory( ArmorUtils.getDecoderStream(inputStream)); diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/util/KeyRingUtils.java b/pgpainless-core/src/main/java/org/pgpainless/key/util/KeyRingUtils.java index 0bcdf8d1..013e283e 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/key/util/KeyRingUtils.java +++ b/pgpainless-core/src/main/java/org/pgpainless/key/util/KeyRingUtils.java @@ -167,12 +167,9 @@ public final class KeyRingUtils { * * @param secretKeyRings secret key ring collection * @return public key ring collection - * @throws PGPException TODO: remove - * @throws IOException TODO: remove */ @Nonnull - public static PGPPublicKeyRingCollection publicKeyRingCollectionFrom(@Nonnull PGPSecretKeyRingCollection secretKeyRings) - throws PGPException, IOException { + public static PGPPublicKeyRingCollection publicKeyRingCollectionFrom(@Nonnull PGPSecretKeyRingCollection secretKeyRings) { List certificates = new ArrayList<>(); for (PGPSecretKeyRing secretKey : secretKeyRings) { certificates.add(PGPainless.extractCertificate(secretKey)); @@ -200,13 +197,9 @@ public final class KeyRingUtils { * * @param rings array of public key rings * @return key ring collection - * - * @throws IOException in case of an io error - * @throws PGPException in case of a broken key */ @Nonnull - public static PGPPublicKeyRingCollection keyRingsToKeyRingCollection(@Nonnull PGPPublicKeyRing... rings) - throws IOException, PGPException { + public static PGPPublicKeyRingCollection keyRingsToKeyRingCollection(@Nonnull PGPPublicKeyRing... rings) { return new PGPPublicKeyRingCollection(Arrays.asList(rings)); } @@ -215,13 +208,9 @@ public final class KeyRingUtils { * * @param rings array of secret key rings * @return secret key ring collection - * - * @throws IOException in case of an io error - * @throws PGPException in case of a broken key */ @Nonnull - public static PGPSecretKeyRingCollection keyRingsToKeyRingCollection(@Nonnull PGPSecretKeyRing... rings) - throws IOException, PGPException { + public static PGPSecretKeyRingCollection keyRingsToKeyRingCollection(@Nonnull PGPSecretKeyRing... rings) { return new PGPSecretKeyRingCollection(Arrays.asList(rings)); } diff --git a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/ModificationDetectionTests.java b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/ModificationDetectionTests.java index 2ca265ea..14a041ff 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/ModificationDetectionTests.java +++ b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/ModificationDetectionTests.java @@ -530,7 +530,7 @@ public class ModificationDetectionTests { ); } - private PGPSecretKeyRingCollection getDecryptionKey() throws IOException, PGPException { + private PGPSecretKeyRingCollection getDecryptionKey() throws IOException { PGPSecretKeyRing secretKeys = PGPainless.readKeyRing().secretKeyRing(keyAscii); return new PGPSecretKeyRingCollection(Collections.singletonList(secretKeys)); } diff --git a/pgpainless-core/src/test/java/org/pgpainless/encryption_signing/EncryptionOptionsTest.java b/pgpainless-core/src/test/java/org/pgpainless/encryption_signing/EncryptionOptionsTest.java index 3e7ccb4d..3436ba69 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/encryption_signing/EncryptionOptionsTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/encryption_signing/EncryptionOptionsTest.java @@ -9,7 +9,6 @@ import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; -import java.io.IOException; import java.security.InvalidAlgorithmParameterException; import java.security.NoSuchAlgorithmException; import java.util.Arrays; @@ -165,7 +164,7 @@ public class EncryptionOptionsTest { } @Test - public void testAddRecipients_PGPPublicKeyRingCollection() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, IOException { + public void testAddRecipients_PGPPublicKeyRingCollection() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException { PGPPublicKeyRing secondKeyRing = KeyRingUtils.publicKeyRingFrom( PGPainless.generateKeyRing().modernKeyRing("other@pgpainless.org")); diff --git a/pgpainless-core/src/test/java/org/pgpainless/key/certification/CertifyCertificateTest.java b/pgpainless-core/src/test/java/org/pgpainless/key/certification/CertifyCertificateTest.java index f837be1b..eb0069f5 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/key/certification/CertifyCertificateTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/key/certification/CertifyCertificateTest.java @@ -140,7 +140,7 @@ public class CertifyCertificateTest { } @Test - public void testScopedDelegation() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, IOException { + public void testScopedDelegation() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException { PGPSecretKeyRing aliceKey = PGPainless.generateKeyRing() .modernKeyRing("Alice "); PGPSecretKeyRing caKey = PGPainless.generateKeyRing() diff --git a/pgpainless-core/src/test/java/org/pgpainless/key/generation/GenerateKeyWithAdditionalUserIdTest.java b/pgpainless-core/src/test/java/org/pgpainless/key/generation/GenerateKeyWithAdditionalUserIdTest.java index 2e2ffc8d..e68d8e9b 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/key/generation/GenerateKeyWithAdditionalUserIdTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/key/generation/GenerateKeyWithAdditionalUserIdTest.java @@ -7,7 +7,6 @@ package org.pgpainless.key.generation; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; -import java.io.IOException; import java.security.InvalidAlgorithmParameterException; import java.security.NoSuchAlgorithmException; import java.util.Date; @@ -26,14 +25,15 @@ import org.pgpainless.key.generation.type.rsa.RsaLength; import org.pgpainless.key.util.KeyRingUtils; import org.pgpainless.key.util.UserId; import org.pgpainless.timeframe.TestTimeFrameProvider; +import org.pgpainless.util.DateUtil; import org.pgpainless.util.TestAllImplementations; public class GenerateKeyWithAdditionalUserIdTest { @TestTemplate @ExtendWith(TestAllImplementations.class) - public void test() throws InvalidAlgorithmParameterException, NoSuchAlgorithmException, PGPException, IOException { - Date now = new Date(); + public void test() throws InvalidAlgorithmParameterException, NoSuchAlgorithmException, PGPException { + Date now = DateUtil.now(); Date expiration = TestTimeFrameProvider.defaultExpirationForCreationDate(now); PGPSecretKeyRing secretKeys = PGPainless.buildKeyRing() .setPrimaryKey(KeySpec.getBuilder( diff --git a/pgpainless-core/src/test/java/org/pgpainless/util/BCUtilTest.java b/pgpainless-core/src/test/java/org/pgpainless/util/BCUtilTest.java index 82368762..821d9e30 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/util/BCUtilTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/util/BCUtilTest.java @@ -8,7 +8,6 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; -import java.io.IOException; import java.security.InvalidAlgorithmParameterException; import java.security.NoSuchAlgorithmException; import java.util.Iterator; @@ -36,8 +35,7 @@ public class BCUtilTest { @Test public void keyRingToCollectionTest() - throws PGPException, NoSuchAlgorithmException, InvalidAlgorithmParameterException, - IOException { + throws PGPException, NoSuchAlgorithmException, InvalidAlgorithmParameterException { PGPSecretKeyRing sec = PGPainless.buildKeyRing() .setPrimaryKey(KeySpec.getBuilder( KeyType.RSA(RsaLength._3072), diff --git a/pgpainless-core/src/test/java/org/pgpainless/util/selection/keyring/KeyRingsFromCollectionTest.java b/pgpainless-core/src/test/java/org/pgpainless/util/selection/keyring/KeyRingsFromCollectionTest.java index eb3510be..bf4955cd 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/util/selection/keyring/KeyRingsFromCollectionTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/util/selection/keyring/KeyRingsFromCollectionTest.java @@ -56,7 +56,7 @@ public class KeyRingsFromCollectionTest { } @Test - public void selectPublicKeyRingFromPublicKeyRingCollectionTest() throws IOException, PGPException { + public void selectPublicKeyRingFromPublicKeyRingCollectionTest() throws IOException { PGPPublicKeyRing emil = TestKeys.getEmilPublicKeyRing(); PGPPublicKeyRing juliet = TestKeys.getJulietPublicKeyRing(); PGPPublicKeyRingCollection collection = new PGPPublicKeyRingCollection(Arrays.asList(emil, juliet)); @@ -68,7 +68,7 @@ public class KeyRingsFromCollectionTest { } @Test - public void selectPublicKeyRingMapFromPublicKeyRingCollectionMapTest() throws IOException, PGPException { + public void selectPublicKeyRingMapFromPublicKeyRingCollectionMapTest() throws IOException { PGPPublicKeyRing emil = TestKeys.getEmilPublicKeyRing(); PGPPublicKeyRing juliet = TestKeys.getJulietPublicKeyRing(); MultiMap map = new MultiMap<>(); diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/DearmorImpl.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/DearmorImpl.java index ac53d415..f0b21a6d 100644 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/DearmorImpl.java +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/DearmorImpl.java @@ -18,7 +18,7 @@ import sop.operation.Dearmor; public class DearmorImpl implements Dearmor { @Override - public Ready data(InputStream data) throws IOException { + public Ready data(InputStream data) { InputStream decoder; try { decoder = PGPUtil.getDecoderStream(data); diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/InlineSignImpl.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/InlineSignImpl.java index d0cfbfcd..30e1d71e 100644 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/InlineSignImpl.java +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/InlineSignImpl.java @@ -71,7 +71,7 @@ public class InlineSignImpl implements InlineSign { } @Override - public Ready data(InputStream data) throws SOPGPException.KeyIsProtected, IOException, SOPGPException.ExpectedText { + public Ready data(InputStream data) throws SOPGPException.KeyIsProtected, SOPGPException.ExpectedText { for (PGPSecretKeyRing key : signingKeys) { try { if (mode == InlineSignAs.clearsigned) { diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/InlineVerifyImpl.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/InlineVerifyImpl.java index 4af53685..81d614cf 100644 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/InlineVerifyImpl.java +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/InlineVerifyImpl.java @@ -49,7 +49,7 @@ public class InlineVerifyImpl implements InlineVerify { } @Override - public ReadyWithResult> data(InputStream data) throws IOException, SOPGPException.NoSignature, SOPGPException.BadData { + public ReadyWithResult> data(InputStream data) throws SOPGPException.NoSignature, SOPGPException.BadData { return new ReadyWithResult>() { @Override public List writeTo(OutputStream outputStream) throws IOException, SOPGPException.NoSignature { From babd1542e3113065270552568f2c96144348cb7b Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 29 Sep 2022 13:02:36 +0200 Subject: [PATCH 032/763] DO NOT MERGE: Disable broken test --- .../java/org/pgpainless/signature/IgnoreMarkerPackets.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pgpainless-core/src/test/java/org/pgpainless/signature/IgnoreMarkerPackets.java b/pgpainless-core/src/test/java/org/pgpainless/signature/IgnoreMarkerPackets.java index c86efd05..87766dfe 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/signature/IgnoreMarkerPackets.java +++ b/pgpainless-core/src/test/java/org/pgpainless/signature/IgnoreMarkerPackets.java @@ -20,6 +20,7 @@ import org.bouncycastle.openpgp.PGPPublicKeyRing; import org.bouncycastle.openpgp.PGPSecretKeyRing; import org.bouncycastle.openpgp.PGPSignature; import org.bouncycastle.util.io.Streams; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.pgpainless.PGPainless; import org.pgpainless.decryption_verification.ConsumerOptions; @@ -159,6 +160,8 @@ public class IgnoreMarkerPackets { } @Test + @Disabled + // TODO: Enable and fix public void markerPlusEncryptedMessage() throws IOException, PGPException { String msg = "-----BEGIN PGP MESSAGE-----\n" + "\n" + From 2ce4486e8936cbbbddf941858b2644b019e2034d Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 29 Sep 2022 13:14:32 +0200 Subject: [PATCH 033/763] Convert links in javadoc to html --- .../java/org/pgpainless/algorithm/PublicKeyAlgorithm.java | 6 +++--- .../decryption_verification/MissingPublicKeyCallback.java | 4 +++- .../key/util/PublicKeyParameterValidationUtil.java | 2 +- .../test/java/org/bouncycastle/PGPPublicKeyRingTest.java | 2 +- .../src/test/java/org/pgpainless/key/WeirdKeys.java | 2 +- .../key/generation/GenerateWithEmptyPassphraseTest.java | 2 +- 6 files changed, 10 insertions(+), 8 deletions(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/algorithm/PublicKeyAlgorithm.java b/pgpainless-core/src/main/java/org/pgpainless/algorithm/PublicKeyAlgorithm.java index baa8f92e..c7599db9 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/algorithm/PublicKeyAlgorithm.java +++ b/pgpainless-core/src/main/java/org/pgpainless/algorithm/PublicKeyAlgorithm.java @@ -28,7 +28,7 @@ public enum PublicKeyAlgorithm { /** * RSA with usage encryption. * - * @deprecated see https://tools.ietf.org/html/rfc4880#section-13.5 + * @deprecated see Deprecation notice */ @Deprecated RSA_ENCRYPT (PublicKeyAlgorithmTags.RSA_ENCRYPT, false, true), @@ -36,7 +36,7 @@ public enum PublicKeyAlgorithm { /** * RSA with usage of creating signatures. * - * @deprecated see https://tools.ietf.org/html/rfc4880#section-13.5 + * @deprecated see Deprecation notice */ @Deprecated RSA_SIGN (PublicKeyAlgorithmTags.RSA_SIGN, true, false), @@ -71,7 +71,7 @@ public enum PublicKeyAlgorithm { /** * ElGamal General. * - * @deprecated see https://tools.ietf.org/html/rfc4880#section-13.8 + * @deprecated see Deprecation notice */ @Deprecated ELGAMAL_GENERAL (PublicKeyAlgorithmTags.ELGAMAL_GENERAL, true, true), diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/MissingPublicKeyCallback.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/MissingPublicKeyCallback.java index f3eb949b..9b6f4a1e 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/MissingPublicKeyCallback.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/MissingPublicKeyCallback.java @@ -20,11 +20,13 @@ public interface MissingPublicKeyCallback { * you may not only search for the key-id on the key rings primary key! * * It would be super cool to provide the OpenPgp fingerprint here, but unfortunately one-pass-signatures - * only contain the key id (see https://datatracker.ietf.org/doc/html/rfc4880#section-5.4) + * only contain the key id. * * @param keyId ID of the missing signing (sub)key * * @return keyring containing the key or null + * + * @see RFC */ @Nullable PGPPublicKeyRing onMissingPublicKeyEncountered(@Nonnull Long keyId); diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/util/PublicKeyParameterValidationUtil.java b/pgpainless-core/src/main/java/org/pgpainless/key/util/PublicKeyParameterValidationUtil.java index d3c7fe68..69940691 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/key/util/PublicKeyParameterValidationUtil.java +++ b/pgpainless-core/src/main/java/org/pgpainless/key/util/PublicKeyParameterValidationUtil.java @@ -237,7 +237,7 @@ public class PublicKeyParameterValidationUtil { * Validate ElGamal public key parameters. * * Original implementation by the openpgpjs authors: - * https://github.com/openpgpjs/openpgpjs/blob/main/src/crypto/public_key/elgamal.js#L76-L143 + * Stackexchange link */ @Test public void subkeysDoNotHaveUserIDsTest() throws InvalidAlgorithmParameterException, NoSuchAlgorithmException, PGPException { diff --git a/pgpainless-core/src/test/java/org/pgpainless/key/WeirdKeys.java b/pgpainless-core/src/test/java/org/pgpainless/key/WeirdKeys.java index b02b07c5..7500bab4 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/key/WeirdKeys.java +++ b/pgpainless-core/src/test/java/org/pgpainless/key/WeirdKeys.java @@ -13,7 +13,7 @@ import org.pgpainless.PGPainless; * This class contains a set of slightly out of spec or weird keys. * Those are used to test whether implementations behave correctly when dealing with such keys. * - * Original source: https://gitlab.com/sequoia-pgp/weird-keys + * @see Original Source */ public class WeirdKeys { diff --git a/pgpainless-core/src/test/java/org/pgpainless/key/generation/GenerateWithEmptyPassphraseTest.java b/pgpainless-core/src/test/java/org/pgpainless/key/generation/GenerateWithEmptyPassphraseTest.java index dbb3f49b..c1375883 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/key/generation/GenerateWithEmptyPassphraseTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/key/generation/GenerateWithEmptyPassphraseTest.java @@ -20,7 +20,7 @@ import org.pgpainless.util.TestAllImplementations; import org.pgpainless.util.Passphrase; /** - * Reproduce behavior of https://github.com/pgpainless/pgpainless/issues/16 + * Reproduce behavior of Date: Thu, 29 Sep 2022 13:15:02 +0200 Subject: [PATCH 035/763] Reformat KeyRingReader --- .../pgpainless/key/parsing/KeyRingReader.java | 67 ++++++++++++------- 1 file changed, 44 insertions(+), 23 deletions(-) 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 bbaecd4f..50cacf05 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 @@ -41,7 +41,8 @@ public class KeyRingReader { * @return key ring * @throws IOException in case of an IO error */ - public PGPKeyRing keyRing(@Nonnull InputStream inputStream) throws IOException { + public PGPKeyRing keyRing(@Nonnull InputStream inputStream) + throws IOException { return readKeyRing(inputStream); } @@ -53,7 +54,8 @@ public class KeyRingReader { * @return key ring * @throws IOException in case of an IO error */ - public PGPKeyRing keyRing(@Nonnull byte[] bytes) throws IOException { + public PGPKeyRing keyRing(@Nonnull byte[] bytes) + throws IOException { return keyRing(new ByteArrayInputStream(bytes)); } @@ -65,19 +67,23 @@ public class KeyRingReader { * @return key ring * @throws IOException in case of an IO error */ - public PGPKeyRing keyRing(@Nonnull String asciiArmored) throws IOException { + public PGPKeyRing keyRing(@Nonnull String asciiArmored) + throws IOException { return keyRing(asciiArmored.getBytes(UTF8)); } - public PGPPublicKeyRing publicKeyRing(@Nonnull InputStream inputStream) throws IOException { + public PGPPublicKeyRing publicKeyRing(@Nonnull InputStream inputStream) + throws IOException { return readPublicKeyRing(inputStream); } - public PGPPublicKeyRing publicKeyRing(@Nonnull byte[] bytes) throws IOException { + public PGPPublicKeyRing publicKeyRing(@Nonnull byte[] bytes) + throws IOException { return publicKeyRing(new ByteArrayInputStream(bytes)); } - public PGPPublicKeyRing publicKeyRing(@Nonnull String asciiArmored) throws IOException { + public PGPPublicKeyRing publicKeyRing(@Nonnull String asciiArmored) + throws IOException { return publicKeyRing(asciiArmored.getBytes(UTF8)); } @@ -86,23 +92,28 @@ public class KeyRingReader { return readPublicKeyRingCollection(inputStream); } - public PGPPublicKeyRingCollection publicKeyRingCollection(@Nonnull byte[] bytes) throws IOException, PGPException { + public PGPPublicKeyRingCollection publicKeyRingCollection(@Nonnull byte[] bytes) + throws IOException, PGPException { return publicKeyRingCollection(new ByteArrayInputStream(bytes)); } - public PGPPublicKeyRingCollection publicKeyRingCollection(@Nonnull String asciiArmored) throws IOException, PGPException { + public PGPPublicKeyRingCollection publicKeyRingCollection(@Nonnull String asciiArmored) + throws IOException, PGPException { return publicKeyRingCollection(asciiArmored.getBytes(UTF8)); } - public PGPSecretKeyRing secretKeyRing(@Nonnull InputStream inputStream) throws IOException { + public PGPSecretKeyRing secretKeyRing(@Nonnull InputStream inputStream) + throws IOException { return readSecretKeyRing(inputStream); } - public PGPSecretKeyRing secretKeyRing(@Nonnull byte[] bytes) throws IOException { + public PGPSecretKeyRing secretKeyRing(@Nonnull byte[] bytes) + throws IOException { return secretKeyRing(new ByteArrayInputStream(bytes)); } - public PGPSecretKeyRing secretKeyRing(@Nonnull String asciiArmored) throws IOException { + public PGPSecretKeyRing secretKeyRing(@Nonnull String asciiArmored) + throws IOException { return secretKeyRing(asciiArmored.getBytes(UTF8)); } @@ -111,11 +122,13 @@ public class KeyRingReader { return readSecretKeyRingCollection(inputStream); } - public PGPSecretKeyRingCollection secretKeyRingCollection(@Nonnull byte[] bytes) throws IOException, PGPException { + public PGPSecretKeyRingCollection secretKeyRingCollection(@Nonnull byte[] bytes) + throws IOException, PGPException { return secretKeyRingCollection(new ByteArrayInputStream(bytes)); } - public PGPSecretKeyRingCollection secretKeyRingCollection(@Nonnull String asciiArmored) throws IOException, PGPException { + public PGPSecretKeyRingCollection secretKeyRingCollection(@Nonnull String asciiArmored) + throws IOException, PGPException { return secretKeyRingCollection(asciiArmored.getBytes(UTF8)); } @@ -124,11 +137,13 @@ public class KeyRingReader { return readKeyRingCollection(inputStream, isSilent); } - public PGPKeyRingCollection keyRingCollection(@Nonnull byte[] bytes, boolean isSilent) throws IOException, PGPException { + public PGPKeyRingCollection keyRingCollection(@Nonnull byte[] bytes, boolean isSilent) + throws IOException, PGPException { return keyRingCollection(new ByteArrayInputStream(bytes), isSilent); } - public PGPKeyRingCollection keyRingCollection(@Nonnull String asciiArmored, boolean isSilent) throws IOException, PGPException { + public PGPKeyRingCollection keyRingCollection(@Nonnull String asciiArmored, boolean isSilent) + throws IOException, PGPException { return keyRingCollection(asciiArmored.getBytes(UTF8), isSilent); } @@ -142,7 +157,8 @@ public class KeyRingReader { * @return key ring * @throws IOException in case of an IO error */ - public static PGPKeyRing readKeyRing(@Nonnull InputStream inputStream) throws IOException { + public static PGPKeyRing readKeyRing(@Nonnull InputStream inputStream) + throws IOException { return readKeyRing(inputStream, MAX_ITERATIONS); } @@ -157,7 +173,8 @@ public class KeyRingReader { * @return key ring * @throws IOException in case of an IO error */ - public static PGPKeyRing readKeyRing(@Nonnull InputStream inputStream, int maxIterations) throws IOException { + public static PGPKeyRing readKeyRing(@Nonnull InputStream inputStream, int maxIterations) + throws IOException { PGPObjectFactory objectFactory = ImplementationFactory.getInstance().getPGPObjectFactory( ArmorUtils.getDecoderStream(inputStream)); int i = 0; @@ -181,7 +198,8 @@ public class KeyRingReader { throw new IOException("Loop exceeded max iteration count."); } - public static PGPPublicKeyRing readPublicKeyRing(@Nonnull InputStream inputStream) throws IOException { + public static PGPPublicKeyRing readPublicKeyRing(@Nonnull InputStream inputStream) + throws IOException { return readPublicKeyRing(inputStream, MAX_ITERATIONS); } @@ -196,7 +214,8 @@ public class KeyRingReader { * * @throws IOException in case of an IO error or exceeding of max iterations */ - public static PGPPublicKeyRing readPublicKeyRing(@Nonnull InputStream inputStream, int maxIterations) throws IOException { + public static PGPPublicKeyRing readPublicKeyRing(@Nonnull InputStream inputStream, int maxIterations) + throws IOException { PGPObjectFactory objectFactory = ImplementationFactory.getInstance().getPGPObjectFactory( ArmorUtils.getDecoderStream(inputStream)); int i = 0; @@ -218,7 +237,7 @@ public class KeyRingReader { } public static PGPPublicKeyRingCollection readPublicKeyRingCollection(@Nonnull InputStream inputStream) - throws IOException, PGPException { + throws IOException { return readPublicKeyRingCollection(inputStream, MAX_ITERATIONS); } @@ -264,7 +283,8 @@ public class KeyRingReader { throw new IOException("Loop exceeded max iteration count."); } - public static PGPSecretKeyRing readSecretKeyRing(@Nonnull InputStream inputStream) throws IOException { + public static PGPSecretKeyRing readSecretKeyRing(@Nonnull InputStream inputStream) + throws IOException { return readSecretKeyRing(inputStream, MAX_ITERATIONS); } @@ -279,7 +299,8 @@ public class KeyRingReader { * * @throws IOException in case of an IO error or exceeding of max iterations */ - public static PGPSecretKeyRing readSecretKeyRing(@Nonnull InputStream inputStream, int maxIterations) throws IOException { + public static PGPSecretKeyRing readSecretKeyRing(@Nonnull InputStream inputStream, int maxIterations) + throws IOException { InputStream decoderStream = ArmorUtils.getDecoderStream(inputStream); PGPObjectFactory objectFactory = ImplementationFactory.getInstance().getPGPObjectFactory(decoderStream); int i = 0; @@ -302,7 +323,7 @@ public class KeyRingReader { } public static PGPSecretKeyRingCollection readSecretKeyRingCollection(@Nonnull InputStream inputStream) - throws IOException, PGPException { + throws IOException { return readSecretKeyRingCollection(inputStream, MAX_ITERATIONS); } From 5e37d8038a4e241b11ca85324762f4d377a6585f Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 29 Sep 2022 17:45:32 +0200 Subject: [PATCH 036/763] WIP: So close to working notarizations --- .../OpenPgpMessageInputStream.java | 359 +++++++++++------- .../TeeBCPGInputStream.java | 1 + .../automaton/PDA.java | 16 +- .../OpenPgpMessageInputStreamTest.java | 145 +++++++ version.gradle | 2 +- 5 files changed, 375 insertions(+), 148 deletions(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java index a1364997..2d031edc 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java @@ -4,12 +4,17 @@ package org.pgpainless.decryption_verification; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.NoSuchElementException; +import java.util.Stack; + import org.bouncycastle.bcpg.BCPGInputStream; -import org.bouncycastle.bcpg.BCPGOutputStream; -import org.bouncycastle.bcpg.OnePassSignaturePacket; -import org.bouncycastle.bcpg.Packet; -import org.bouncycastle.bcpg.PacketTags; -import org.bouncycastle.bcpg.SignaturePacket; import org.bouncycastle.openpgp.PGPCompressedData; import org.bouncycastle.openpgp.PGPEncryptedData; import org.bouncycastle.openpgp.PGPEncryptedDataList; @@ -17,7 +22,6 @@ 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; @@ -26,11 +30,12 @@ import org.bouncycastle.openpgp.PGPPublicKeyRing; import org.bouncycastle.openpgp.PGPSecretKey; import org.bouncycastle.openpgp.PGPSecretKeyRing; import org.bouncycastle.openpgp.PGPSignature; -import org.bouncycastle.openpgp.PGPSignatureList; +import org.bouncycastle.openpgp.PGPUtil; import org.bouncycastle.openpgp.operator.PBEDataDecryptorFactory; import org.bouncycastle.openpgp.operator.PGPContentVerifierBuilderProvider; import org.bouncycastle.openpgp.operator.PublicKeyDataDecryptorFactory; import org.bouncycastle.openpgp.operator.SessionKeyDataDecryptorFactory; +import org.bouncycastle.util.encoders.Hex; import org.pgpainless.PGPainless; import org.pgpainless.algorithm.CompressionAlgorithm; import org.pgpainless.algorithm.EncryptionPurpose; @@ -54,15 +59,6 @@ import org.pgpainless.util.Tuple; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.util.ArrayList; -import java.util.Collection; -import java.util.List; -import java.util.Stack; - public class OpenPgpMessageInputStream extends InputStream { private static final Logger LOGGER = LoggerFactory.getLogger(OpenPgpMessageInputStream.class); @@ -100,13 +96,7 @@ public class OpenPgpMessageInputStream extends InputStream { this.signatures.addDetachedSignatures(options.getDetachedSignatures()); } - // TODO: Use BCPGInputStream.wrap(inputStream); - BCPGInputStream bcpg = null; - if (inputStream instanceof BCPGInputStream) { - bcpg = (BCPGInputStream) inputStream; - } else { - bcpg = new BCPGInputStream(inputStream); - } + BCPGInputStream bcpg = BCPGInputStream.wrap(inputStream); this.packetInputStream = new TeeBCPGInputStream(bcpg, signatures); // *omnomnom* @@ -133,13 +123,20 @@ public class OpenPgpMessageInputStream extends InputStream { throws IOException, PGPException { int tag; loop: while ((tag = nextTag()) != -1) { - OpenPgpPacket nextPacket = OpenPgpPacket.requireFromTag(tag); + OpenPgpPacket nextPacket; + try { + nextPacket = OpenPgpPacket.requireFromTag(tag); + } catch (NoSuchElementException e) { + log("Invalid tag: " + tag); + throw e; + } + log(nextPacket.toString()); + signatures.nextPacket(nextPacket); switch (nextPacket) { // Literal Data - the literal data content is the new input stream case LIT: automaton.next(InputAlphabet.LiteralData); - signatures.commitNested(); processLiteralData(); break loop; @@ -153,12 +150,8 @@ public class OpenPgpMessageInputStream extends InputStream { // One Pass Signature case OPS: automaton.next(InputAlphabet.OnePassSignatures); - // signatures.addOnePassSignature(readOnePassSignature()); - PGPOnePassSignatureList onePassSignatureList = readOnePassSignatures(); - for (PGPOnePassSignature ops : onePassSignatureList) { - signatures.addOnePassSignature(ops); - } - // signatures.addOnePassSignatures(readOnePassSignatures()); + PGPOnePassSignature onePassSignature = readOnePassSignature(); + signatures.addOnePassSignature(onePassSignature); break; // Signature - either prepended to the message, or corresponding to a One Pass Signature @@ -166,13 +159,8 @@ public class OpenPgpMessageInputStream extends InputStream { boolean isSigForOPS = automaton.peekStack() == StackAlphabet.ops; automaton.next(InputAlphabet.Signatures); - // PGPSignature signature = readSignature(); - // processSignature(signature, isSigForOPS); - - PGPSignatureList signatureList = readSignatures(); - for (PGPSignature signature : signatureList) { - processSignature(signature, isSigForOPS); - } + PGPSignature signature = readSignature(); + processSignature(signature, isSigForOPS); break; @@ -220,6 +208,7 @@ public class OpenPgpMessageInputStream extends InputStream { private void processSignature(PGPSignature signature, boolean isSigForOPS) { if (isSigForOPS) { + signatures.popNested(); signatures.addCorrespondingOnePassSignature(signature); } else { signatures.addPrependedSignature(signature); @@ -240,6 +229,41 @@ public class OpenPgpMessageInputStream extends InputStream { nestedInputStream = literalData.getDataStream(); } + private void debugEncryptedData() throws PGPException, IOException { + PGPEncryptedDataList encDataList = new PGPEncryptedDataList(packetInputStream); + + // TODO: Replace with !encDataList.isIntegrityProtected() + if (!encDataList.get(0).isIntegrityProtected()) { + throw new MessageNotIntegrityProtectedException(); + } + + SortedESKs esks = new SortedESKs(encDataList); + for (PGPPublicKeyEncryptedData pkesk : esks.pkesks) { + long keyId = pkesk.getKeyID(); + PGPSecretKeyRing decryptionKeys = getDecryptionKey(keyId); + if (decryptionKeys == null) { + continue; + } + SecretKeyRingProtector protector = options.getSecretKeyProtector(decryptionKeys); + PGPSecretKey decryptionKey = decryptionKeys.getSecretKey(keyId); + PGPPrivateKey privateKey = UnlockSecretKey.unlockSecretKey(decryptionKey, protector); + + PublicKeyDataDecryptorFactory decryptorFactory = ImplementationFactory.getInstance() + .getPublicKeyDataDecryptorFactory(privateKey); + try { + InputStream decrypted = pkesk.getDataStream(decryptorFactory); + InputStream decoder = PGPUtil.getDecoderStream(decrypted); + PGPObjectFactory objectFactory = ImplementationFactory.getInstance() + .getPGPObjectFactory(decoder); + objectFactory.nextObject(); + objectFactory.nextObject(); + objectFactory.nextObject(); + } catch (PGPException e) { + // hm :/ + } + } + } + private boolean processEncryptedData() throws IOException, PGPException { PGPEncryptedDataList encDataList = new PGPEncryptedDataList(packetInputStream); @@ -309,7 +333,8 @@ public class OpenPgpMessageInputStream extends InputStream { InputStream decrypted = pkesk.getDataStream(decryptorFactory); MessageMetadata.EncryptedData encryptedData = new MessageMetadata.EncryptedData( SymmetricKeyAlgorithm.requireFromId(pkesk.getSymmetricAlgorithm(decryptorFactory))); - nestedInputStream = new OpenPgpMessageInputStream(decrypted, options, encryptedData); + + nestedInputStream = new OpenPgpMessageInputStream(PGPUtil.getDecoderStream(decrypted), options, encryptedData); return true; } catch (PGPException e) { // hm :/ @@ -382,52 +407,12 @@ public class OpenPgpMessageInputStream extends InputStream { private PGPOnePassSignature readOnePassSignature() throws PGPException, IOException { - //return new PGPOnePassSignature(packetInputStream); - return null; + return new PGPOnePassSignature(packetInputStream); } private PGPSignature readSignature() throws PGPException, IOException { - //return new PGPSignature(packetInputStream); - return null; - } - - private PGPOnePassSignatureList readOnePassSignatures() throws IOException { - ByteArrayOutputStream buf = new ByteArrayOutputStream(); - BCPGOutputStream bcpgOut = new BCPGOutputStream(buf); - int tag; - while ((tag = nextTag()) == PacketTags.ONE_PASS_SIGNATURE || tag == PacketTags.MARKER) { - Packet packet = packetInputStream.readPacket(); - if (tag == PacketTags.ONE_PASS_SIGNATURE) { - OnePassSignaturePacket sigPacket = (OnePassSignaturePacket) packet; - byte[] bytes = sigPacket.getEncoded(); - bcpgOut.write(bytes); - } - } - bcpgOut.close(); - - PGPObjectFactory objectFactory = ImplementationFactory.getInstance().getPGPObjectFactory(buf.toByteArray()); - PGPOnePassSignatureList signatureList = (PGPOnePassSignatureList) objectFactory.nextObject(); - return signatureList; - } - - private PGPSignatureList readSignatures() throws IOException { - ByteArrayOutputStream buf = new ByteArrayOutputStream(); - BCPGOutputStream bcpgOut = new BCPGOutputStream(buf); - int tag = nextTag(); - while (tag == PacketTags.SIGNATURE || tag == PacketTags.MARKER) { - Packet packet = packetInputStream.readPacket(); - if (tag == PacketTags.SIGNATURE) { - SignaturePacket sigPacket = (SignaturePacket) packet; - sigPacket.encode(bcpgOut); - tag = nextTag(); - } - } - bcpgOut.close(); - - PGPObjectFactory objectFactory = ImplementationFactory.getInstance().getPGPObjectFactory(buf.toByteArray()); - PGPSignatureList signatureList = (PGPSignatureList) objectFactory.nextObject(); - return signatureList; + return new PGPSignature(packetInputStream); } @Override @@ -446,12 +431,11 @@ public class OpenPgpMessageInputStream extends InputStream { boolean eos = r == -1; if (!eos) { byte b = (byte) r; - signatures.update(b); + signatures.updateLiteral(b); } else { nestedInputStream.close(); collectMetadata(); nestedInputStream = null; - signatures.popNested(); try { consumePackets(); @@ -473,11 +457,13 @@ public class OpenPgpMessageInputStream extends InputStream { } int r = nestedInputStream.read(b, off, len); - if (r == -1) { + if (r != -1) { + signatures.updateLiteral(b, off, r); + } + else { nestedInputStream.close(); collectMetadata(); nestedInputStream = null; - signatures.popNested(); try { consumePackets(); @@ -569,14 +555,11 @@ public class OpenPgpMessageInputStream extends InputStream { final ConsumerOptions options; final List detachedSignatures; final List prependedSignatures; - final List onePassSignatures; - final Stack> opsUpdateStack; + final List onePassSignatures; + final Stack> opsUpdateStack; + List literalOPS = new ArrayList<>(); final List correspondingSignatures; - ByteArrayOutputStream out = new ByteArrayOutputStream(); - - List opsCurrentNesting = new ArrayList<>(); - private Signatures(ConsumerOptions options) { this.options = options; this.detachedSignatures = new ArrayList<>(); @@ -608,57 +591,42 @@ public class OpenPgpMessageInputStream extends InputStream { void addOnePassSignature(PGPOnePassSignature signature) { PGPPublicKeyRing certificate = findCertificate(signature.getKeyID()); - initialize(signature, certificate); - onePassSignatures.add(signature); + OPS ops = new OPS(signature); + ops.init(certificate); + onePassSignatures.add(ops); - opsCurrentNesting.add(signature); - if (isContaining(signature)) { + literalOPS.add(ops); + if (signature.isContaining()) { commitNested(); } } - boolean isContaining(PGPOnePassSignature ops) { - try { - byte[] bytes = ops.getEncoded(); - return bytes[bytes.length - 1] == 1; - } catch (IOException e) { - return false; - } - } - void addCorrespondingOnePassSignature(PGPSignature signature) { - for (PGPOnePassSignature onePassSignature : onePassSignatures) { - if (onePassSignature.getKeyID() != signature.getKeyID()) { + for (int i = onePassSignatures.size() - 1; i >= 0; i--) { + OPS onePassSignature = onePassSignatures.get(i); + if (onePassSignature.signature.getKeyID() != signature.getKeyID()) { + continue; + } + if (onePassSignature.finished) { continue; } - boolean verified = false; - try { - verified = onePassSignature.verify(signature); - } catch (PGPException e) { - log("Cannot verify OPS signature.", e); - } - log("One-Pass-Signature by " + Long.toHexString(onePassSignature.getKeyID()) + " is " + (verified ? "verified" : "unverified")); - try { - log(ArmorUtils.toAsciiArmoredString(out.toByteArray())); - } catch (IOException e) { - throw new RuntimeException(e); - } + boolean verified = onePassSignature.verify(signature); + log("One-Pass-Signature by " + Long.toHexString(onePassSignature.signature.getKeyID()) + " is " + (verified ? "verified" : "unverified")); + System.out.println(onePassSignature); + break; } } void commitNested() { - if (opsCurrentNesting.isEmpty()) { - return; - } - - log("Committing " + opsCurrentNesting.size() + " OPS sigs for updating"); - opsUpdateStack.push(opsCurrentNesting); - opsCurrentNesting = new ArrayList<>(); + opsUpdateStack.push(literalOPS); + literalOPS = new ArrayList<>(); } void popNested() { - log("Popping nested"); + if (opsUpdateStack.isEmpty()) { + return; + } opsUpdateStack.pop(); } @@ -676,7 +644,7 @@ public class OpenPgpMessageInputStream extends InputStream { } } - private void initialize(PGPOnePassSignature ops, PGPPublicKeyRing certificate) { + private static void initialize(PGPOnePassSignature ops, PGPPublicKeyRing certificate) { if (certificate == null) { return; } @@ -699,20 +667,9 @@ public class OpenPgpMessageInputStream extends InputStream { return null; // TODO: Missing cert for sig } - public void update(byte b) { - if (!opsUpdateStack.isEmpty()) { - log("Update"); - out.write(b); - } - - for (PGPSignature prepended : prependedSignatures) { - prepended.update(b); - } - - for (List opss : opsUpdateStack) { - for (PGPOnePassSignature ops : opss) { - ops.update(b); - } + public void updateLiteral(byte b) { + for (OPS ops : literalOPS) { + ops.update(b); } for (PGPSignature detached : detachedSignatures) { @@ -720,6 +677,33 @@ public class OpenPgpMessageInputStream extends InputStream { } } + public void updateLiteral(byte[] b, int off, int len) { + for (OPS ops : literalOPS) { + ops.update(b, off, len); + } + + for (PGPSignature detached : detachedSignatures) { + detached.update(b, off, len); + } + } + + public void updatePacket(byte b) { + for (List nestedOPSs : opsUpdateStack) { + for (OPS ops : nestedOPSs) { + ops.update(b); + } + } + } + + public void updatePacket(byte[] buf, int off, int len) { + for (int i = opsUpdateStack.size() - 1; i >= 0; i--) { + List nestedOPSs = opsUpdateStack.get(i); + for (OPS ops : nestedOPSs) { + ops.update(buf, off, len); + } + } + } + public void finish() { for (PGPSignature detached : detachedSignatures) { boolean verified = false; @@ -743,8 +727,97 @@ public class OpenPgpMessageInputStream extends InputStream { } @Override - public void write(int b) throws IOException { - update((byte) b); + public void write(int b) { + updatePacket((byte) b); + } + + @Override + public void write(byte[] b, int off, int len) { + updatePacket(b, off, len); + } + + public void nextPacket(OpenPgpPacket nextPacket) { + if (nextPacket == OpenPgpPacket.LIT) { + if (literalOPS.isEmpty() && !opsUpdateStack.isEmpty()) { + literalOPS = opsUpdateStack.pop(); + } + } + } + + static class OPS { + ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + PGPOnePassSignature signature; + boolean finished; + boolean valid; + + public OPS(PGPOnePassSignature signature) { + this.signature = signature; + } + + public void init(PGPPublicKeyRing certificate) { + initialize(signature, certificate); + } + + public boolean verify(PGPSignature signature) { + if (this.signature.getKeyID() != signature.getKeyID()) { + // nope + return false; + } + finished = true; + try { + valid = this.signature.verify(signature); + } catch (PGPException e) { + log("Cannot verify OPS " + signature.getKeyID()); + } + return valid; + } + + public void update(byte b) { + if (finished) { + log("Updating finished sig!"); + return; + } + signature.update(b); + bytes.write(b); + } + + public void update(byte[] bytes, int off, int len) { + if (finished) { + log("Updating finished sig!"); + return; + } + signature.update(bytes, off, len); + this.bytes.write(bytes, off, len); + } + + @Override + public String toString() { + String OPS = "c40d03000a01fbfcc82a015e733001"; + String LIT_H = "cb28620000000000"; + String LIT = "656e637279707420e28898207369676e20e28898207369676e20e28898207369676e"; + String SIG1 = "c2c10400010a006f058262c806350910fbfcc82a015e7330471400000000001e002073616c74406e6f746174696f6e732e736571756f69612d7067702e6f7267b0409ed8ea96dac66447bdff5b7b60c9f80a0ab91d257029153dc3b6d8c27b98162104d1a66e1a23b182c9980f788cfbfcc82a015e7330000029640c00846b5096d92474fd446cc7edaf9f14572cab93a80e12384c1e829f95debc6e8373c2ce5402be53dc1a18cf92a0ed909e0fb38855713ef8ffb13502ffac7c830fa254cc1aa6c666a97b0cc3bc176538f6913d3b8e8981a65cc42df10e0f39e4d0a06dfe961437b59a71892f4fca1116aed15123ea0d86a7b2ce47dd9d3ef22d920631bc011e82babe03ad5d72b3ba7f95bf646f20ccf6f7a4d95de37397c76c7d53741458e51ab6074007f61181c7b88b7c98f5b7510c8dfa3be01f4841501679478b15c5249d928e2a10d15ec63efa1500b994d5bfb32ffb174a976116930eb97a111e6dfd4c5e43e04a5d76ba74806a62fda63a8c3f53f6eebaf852892340e81dd08bbf348454a2cf525aeb512cf33aeeee78465ee4c442e41cc45ac4e3bb0c3333677aa60332ee7f464d9020f8554b82d619872477cca18d8431888f4ae8abe5894e9720f759c410cd7991db12703dc147040dd0d3758223e0b75de6ceae49c1a0c2c45efedeb7114ae785cc886afdc45c82172e4476e1ab5b86dc4314dd76"; + String SIG2 = "c2c10400010a006f058262c806350910fbfcc82a015e7330471400000000001e002073616c74406e6f746174696f6e732e736571756f69612d7067702e6f7267a4d9c117dc7ba3a7e9270856f128d2ab271743eac3cb5750b22a89bd5fd60753162104d1a66e1a23b182c9980f788cfbfcc82a015e73300000b8400bff796c20fa8b25ff7a42686338e06417a2966e85a0fc2723c928bef6cd19d34cf5e7d55ada33080613012dadb79e0278e59d9e7ed7d2d6102912a5f768c2e75b60099225c3d8bfe0c123240188b80dbee89b9b3bd5b13ccc662abc37e2129b6968adac9aba43aa778c0fe4fe337591ee87a96a29a013debc83555293c877144fc676aa1b03782c501949521a320adf6ad96c4f2e036b52a18369c637fdc49033696a84d03a69580b953187fce5aca6fb26fc8815da9f3b513bfe8e304f33ecb4b521aeb7d09c4a284ea66123bd0d6a358b2526d762ca110e1f7f20b3038d774b64d5cfd34e2213765828359d7afc5bf24d5270e99d80c3c1568fa01624b6ea1e9ce4e6890ce9bacf6611a45d41e2671f68f5b096446bf08d27ce75608425b2e3ab92146229ad1fcd8224aca5b5f73960506e7df07bfbf3664348e8ecbfb2eb467b9cfe412cb377a6ee2eb5fd11be9cf9208fe9a74c296f52cfa02a1eb0519ad9a8349bf6ccd6495feb7e391451bf96e08a0798883dee5974e47cbf3b51f111b6d3"; + String out = signature.getKeyID() + " last=" + signature.isContaining() + "\n"; + + String hex = Hex.toHexString(bytes.toByteArray()); + while (hex.contains(OPS)) { + hex = hex.replace(OPS, "[OPS]"); + } + while (hex.contains(LIT_H)) { + hex = hex.replace(LIT_H, "[LIT]"); + } + while (hex.contains(LIT)) { + hex = hex.replace(LIT, ""); + } + while (hex.contains(SIG1)) { + hex = hex.replace(SIG1, "[SIG1]"); + } + while (hex.contains(SIG2)) { + hex = hex.replace(SIG2, "[SIG2]"); + } + + return out + hex; + } } } diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/TeeBCPGInputStream.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/TeeBCPGInputStream.java index ab24f22e..cce21051 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/TeeBCPGInputStream.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/TeeBCPGInputStream.java @@ -1,6 +1,7 @@ package org.pgpainless.decryption_verification; import org.bouncycastle.bcpg.BCPGInputStream; +import org.pgpainless.util.ArmorUtils; import java.io.IOException; import java.io.InputStream; diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/automaton/PDA.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/automaton/PDA.java index dda2adce..1d4b1189 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/automaton/PDA.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/automaton/PDA.java @@ -149,11 +149,19 @@ public class PDA { @Override State transition(InputAlphabet input, PDA automaton) throws MalformedOpenPgpMessageException { StackAlphabet stackItem = automaton.popStack(); - if (stackItem == terminus && input == InputAlphabet.EndOfSequence && automaton.stack.isEmpty()) { - return Valid; - } else { - throw new MalformedOpenPgpMessageException(this, input, stackItem); + if (input == InputAlphabet.EndOfSequence) { + if (stackItem == terminus && automaton.stack.isEmpty()) { + return Valid; + } else { + // premature end of stream + throw new MalformedOpenPgpMessageException(this, input, stackItem); + } + } else if (input == InputAlphabet.Signatures) { + if (stackItem == ops) { + return CorrespondingSignature; + } } + throw new MalformedOpenPgpMessageException(this, input, stackItem); } }, diff --git a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStreamTest.java b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStreamTest.java index e52a6016..f780b2eb 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStreamTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStreamTest.java @@ -433,6 +433,151 @@ public class OpenPgpMessageInputStreamTest { assertNull(metadata.getCompressionAlgorithm()); } + @ParameterizedTest(name = "Process PENC(OPS OPS OPS LIT SIG SIG SIG) using {0}") + @MethodSource("provideMessageProcessors") + public void testProcessOPS_OPS_OPS_LIT_SIG_SIG_SIG(Processor processor) throws IOException, PGPException { + String KEY = "-----BEGIN PGP PRIVATE KEY BLOCK-----\n" + + "Comment: Bob's OpenPGP Transferable Secret Key\n" + + "\n" + + "lQVYBF2lnPIBDAC5cL9PQoQLTMuhjbYvb4Ncuuo0bfmgPRFywX53jPhoFf4Zg6mv\n" + + "/seOXpgecTdOcVttfzC8ycIKrt3aQTiwOG/ctaR4Bk/t6ayNFfdUNxHWk4WCKzdz\n" + + "/56fW2O0F23qIRd8UUJp5IIlN4RDdRCtdhVQIAuzvp2oVy/LaS2kxQoKvph/5pQ/\n" + + "5whqsyroEWDJoSV0yOb25B/iwk/pLUFoyhDG9bj0kIzDxrEqW+7Ba8nocQlecMF3\n" + + "X5KMN5kp2zraLv9dlBBpWW43XktjcCZgMy20SouraVma8Je/ECwUWYUiAZxLIlMv\n" + + "9CurEOtxUw6N3RdOtLmYZS9uEnn5y1UkF88o8Nku890uk6BrewFzJyLAx5wRZ4F0\n" + + "qV/yq36UWQ0JB/AUGhHVPdFf6pl6eaxBwT5GXvbBUibtf8YI2og5RsgTWtXfU7eb\n" + + "SGXrl5ZMpbA6mbfhd0R8aPxWfmDWiIOhBufhMCvUHh1sApMKVZnvIff9/0Dca3wb\n" + + "vLIwa3T4CyshfT0AEQEAAQAL/RZqbJW2IqQDCnJi4Ozm++gPqBPiX1RhTWSjwxfM\n" + + "cJKUZfzLj414rMKm6Jh1cwwGY9jekROhB9WmwaaKT8HtcIgrZNAlYzANGRCM4TLK\n" + + "3VskxfSwKKna8l+s+mZglqbAjUg3wmFuf9Tj2xcUZYmyRm1DEmcN2ZzpvRtHgX7z\n" + + "Wn1mAKUlSDJZSQks0zjuMNbupcpyJokdlkUg2+wBznBOTKzgMxVNC9b2g5/tMPUs\n" + + "hGGWmF1UH+7AHMTaS6dlmr2ZBIyogdnfUqdNg5sZwsxSNrbglKP4sqe7X61uEAIQ\n" + + "bD7rT3LonLbhkrj3I8wilUD8usIwt5IecoHhd9HziqZjRCc1BUBkboUEoyedbDV4\n" + + "i4qfsFZ6CEWoLuD5pW7dEp0M+WeuHXO164Rc+LnH6i1VQrpb1Okl4qO6ejIpIjBI\n" + + "1t3GshtUu/mwGBBxs60KBX5g77mFQ9lLCRj8lSYqOsHRKBhUp4qM869VA+fD0BRP\n" + + "fqPT0I9IH4Oa/A3jYJcg622GwQYA1LhnP208Waf6PkQSJ6kyr8ymY1yVh9VBE/g6\n" + + "fRDYA+pkqKnw9wfH2Qho3ysAA+OmVOX8Hldg+Pc0Zs0e5pCavb0En8iFLvTA0Q2E\n" + + "LR5rLue9uD7aFuKFU/VdcddY9Ww/vo4k5p/tVGp7F8RYCFn9rSjIWbfvvZi1q5Tx\n" + + "+akoZbga+4qQ4WYzB/obdX6SCmi6BndcQ1QdjCCQU6gpYx0MddVERbIp9+2SXDyL\n" + + "hpxjSyz+RGsZi/9UAshT4txP4+MZBgDfK3ZqtW+h2/eMRxkANqOJpxSjMyLO/FXN\n" + + "WxzTDYeWtHNYiAlOwlQZEPOydZFty9IVzzNFQCIUCGjQ/nNyhw7adSgUk3+BXEx/\n" + + "MyJPYY0BYuhLxLYcrfQ9nrhaVKxRJj25SVHj2ASsiwGJRZW4CC3uw40OYxfKEvNC\n" + + "mer/VxM3kg8qqGf9KUzJ1dVdAvjyx2Hz6jY2qWCyRQ6IMjWHyd43C4r3jxooYKUC\n" + + "YnstRQyb/gCSKahveSEjo07CiXMr88UGALwzEr3npFAsPW3osGaFLj49y1oRe11E\n" + + "he9gCHFm+fuzbXrWmdPjYU5/ZdqdojzDqfu4ThfnipknpVUM1o6MQqkjM896FHm8\n" + + "zbKVFSMhEP6DPHSCexMFrrSgN03PdwHTO6iBaIBBFqmGY01tmJ03SxvSpiBPON9P\n" + + "NVvy/6UZFedTq8A07OUAxO62YUSNtT5pmK2vzs3SAZJmbFbMh+NN204TRI72GlqT\n" + + "t5hcfkuv8hrmwPS/ZR6q312mKQ6w/1pqO9qitCFCb2IgQmFiYmFnZSA8Ym9iQG9w\n" + + "ZW5wZ3AuZXhhbXBsZT6JAc4EEwEKADgCGwMFCwkIBwIGFQoJCAsCBBYCAwECHgEC\n" + + "F4AWIQTRpm4aI7GCyZgPeIz7/MgqAV5zMAUCXaWe+gAKCRD7/MgqAV5zMG9sC/9U\n" + + "2T3RrqEbw533FPNfEflhEVRIZ8gDXKM8hU6cqqEzCmzZT6xYTe6sv4y+PJBGXJFX\n" + + "yhj0g6FDkSyboM5litOcTupURObVqMgA/Y4UKERznm4fzzH9qek85c4ljtLyNufe\n" + + "doL2pp3vkGtn7eD0QFRaLLmnxPKQ/TlZKdLE1G3u8Uot8QHicaR6GnAdc5UXQJE3\n" + + "BiV7jZuDyWmZ1cUNwJkKL6oRtp+ZNDOQCrLNLecKHcgCqrpjSQG5oouba1I1Q6Vl\n" + + "sP44dhA1nkmLHtxlTOzpeHj4jnk1FaXmyasurrrI5CgU/L2Oi39DGKTH/A/cywDN\n" + + "4ZplIQ9zR8enkbXquUZvFDe+Xz+6xRXtb5MwQyWODB3nHw85HocLwRoIN9WdQEI+\n" + + "L8a/56AuOwhs8llkSuiITjR7r9SgKJC2WlAHl7E8lhJ3VDW3ELC56KH308d6mwOG\n" + + "ZRAqIAKzM1T5FGjMBhq7ZV0eqdEntBh3EcOIfj2M8rg1MzJv+0mHZOIjByawikad\n" + + "BVgEXaWc8gEMANYwv1xsYyunXYK0X1vY/rP1NNPvhLyLIE7NpK90YNBj+xS1ldGD\n" + + "bUdZqZeef2xJe8gMQg05DoD1DF3GipZ0Ies65beh+d5hegb7N4pzh0LzrBrVNHar\n" + + "29b5ExdI7i4iYD5TO6Vr/qTUOiAN/byqELEzAb+L+b2DVz/RoCm4PIp1DU9ewcc2\n" + + "WB38Ofqut3nLYA5tqJ9XvAiEQme+qAVcM3ZFcaMt4I4dXhDZZNg+D9LiTWcxdUPB\n" + + "leu8iwDRjAgyAhPzpFp+nWoqWA81uIiULWD1Fj+IVoY3ZvgivoYOiEFBJ9lbb4te\n" + + "g9m5UT/AaVDTWuHzbspVlbiVe+qyB77C2daWzNyx6UYBPLOo4r0t0c91kbNE5lgj\n" + + "Z7xz6los0N1U8vq91EFSeQJoSQ62XWavYmlCLmdNT6BNfgh4icLsT7Vr1QMX9jzn\n" + + "JtTPxdXytSdHvpSpULsqJ016l0dtmONcK3z9mj5N5z0k1tg1AH970TGYOe2aUcSx\n" + + "IRDMXDOPyzEfjwARAQABAAv9F2CwsjS+Sjh1M1vegJbZjei4gF1HHpEM0K0PSXsp\n" + + "SfVvpR4AoSJ4He6CXSMWg0ot8XKtDuZoV9jnJaES5UL9pMAD7JwIOqZm/DYVJM5h\n" + + "OASCh1c356/wSbFbzRHPtUdZO9Q30WFNJM5pHbCJPjtNoRmRGkf71RxtvHBzy7np\n" + + "Ga+W6U/NVKHw0i0CYwMI0YlKDakYW3Pm+QL+gHZFvngGweTod0f9l2VLLAmeQR/c\n" + + "+EZs7lNumhuZ8mXcwhUc9JQIhOkpO+wreDysEFkAcsKbkQP3UDUsA1gFx9pbMzT0\n" + + "tr1oZq2a4QBtxShHzP/ph7KLpN+6qtjks3xB/yjTgaGmtrwM8tSe0wD1RwXS+/1o\n" + + "BHpXTnQ7TfeOGUAu4KCoOQLv6ELpKWbRBLWuiPwMdbGpvVFALO8+kvKAg9/r+/ny\n" + + "zM2GQHY+J3Jh5JxPiJnHfXNZjIKLbFbIPdSKNyJBuazXW8xIa//mEHMI5OcvsZBK\n" + + "clAIp7LXzjEjKXIwHwDcTn9pBgDpdOKTHOtJ3JUKx0rWVsDH6wq6iKV/FTVSY5jl\n" + + "zN+puOEsskF1Lfxn9JsJihAVO3yNsp6RvkKtyNlFazaCVKtDAmkjoh60XNxcNRqr\n" + + "gCnwdpbgdHP6v/hvZY54ZaJjz6L2e8unNEkYLxDt8cmAyGPgH2XgL7giHIp9jrsQ\n" + + "aS381gnYwNX6wE1aEikgtY91nqJjwPlibF9avSyYQoMtEqM/1UjTjB2KdD/MitK5\n" + + "fP0VpvuXpNYZedmyq4UOMwdkiNMGAOrfmOeT0olgLrTMT5H97Cn3Yxbk13uXHNu/\n" + + "ZUZZNe8s+QtuLfUlKAJtLEUutN33TlWQY522FV0m17S+b80xJib3yZVJteVurrh5\n" + + "HSWHAM+zghQAvCesg5CLXa2dNMkTCmZKgCBvfDLZuZbjFwnwCI6u/NhOY9egKuUf\n" + + "SA/je/RXaT8m5VxLYMxwqQXKApzD87fv0tLPlVIEvjEsaf992tFEFSNPcG1l/jpd\n" + + "5AVXw6kKuf85UkJtYR1x2MkQDrqY1QX/XMw00kt8y9kMZUre19aCArcmor+hDhRJ\n" + + "E3Gt4QJrD9z/bICESw4b4z2DbgD/Xz9IXsA/r9cKiM1h5QMtXvuhyfVeM01enhxM\n" + + "GbOH3gjqqGNKysx0UODGEwr6AV9hAd8RWXMchJLaExK9J5SRawSg671ObAU24SdY\n" + + "vMQ9Z4kAQ2+1ReUZzf3ogSMRZtMT+d18gT6L90/y+APZIaoArLPhebIAGq39HLmJ\n" + + "26x3z0WAgrpA1kNsjXEXkoiZGPLKIGoe3hqJAbYEGAEKACAWIQTRpm4aI7GCyZgP\n" + + "eIz7/MgqAV5zMAUCXaWc8gIbDAAKCRD7/MgqAV5zMOn/C/9ugt+HZIwX308zI+QX\n" + + "c5vDLReuzmJ3ieE0DMO/uNSC+K1XEioSIZP91HeZJ2kbT9nn9fuReuoff0T0Dief\n" + + "rbwcIQQHFFkrqSp1K3VWmUGp2JrUsXFVdjy/fkBIjTd7c5boWljv/6wAsSfiv2V0\n" + + "JSM8EFU6TYXxswGjFVfc6X97tJNeIrXL+mpSmPPqy2bztcCCHkWS5lNLWQw+R7Vg\n" + + "71Fe6yBSNVrqC2/imYG2J9zlowjx1XU63Wdgqp2Wxt0l8OmsB/W80S1fRF5G4SDH\n" + + "s9HXglXXqPsBRZJYfP+VStm9L5P/sKjCcX6WtZR7yS6G8zj/X767MLK/djANvpPd\n" + + "NVniEke6hM3CNBXYPAMhQBMWhCulcoz+0lxi8L34rMN+Dsbma96psdUrn7uLaB91\n" + + "6we0CTfF8qqm7BsVAgalon/UUiuMY80U3ueoj3okiSTiHIjD/YtpXSPioC8nMng7\n" + + "xqAY9Bwizt4FWgXuLm1a4+So4V9j1TRCXd12Uc2l2RNmgDE=\n" + + "=miES\n" + + "-----END PGP PRIVATE KEY BLOCK-----"; + String MSG = "-----BEGIN PGP MESSAGE-----\n" + + "\n" + + "wcDMA3wvqk35PDeyAQwA0yaEgydkAMEfl7rDTYVGanLKiFiWIs34mkF+LB8qR5eY\n" + + "ZRuhodPbX9QjpgOZ8fETPU3DEvzOaR0kMqKAHl7mmP0inydK5vpx0U0JTHIZkNeC\n" + + "rQbizphG2VA8fUvxZ79bZAe43uguITI2R2EZgEzeq6vCO7Ca4XqK95OADKZoVzS7\n" + + "0uBSMIgAVumrAj2l1ZOYbiIevx0+xJT2NvsLj7TV3ewBIyUg2f5NujcgEnuhpsMu\n" + + "wM/k58u4iBLAa8Qr2f8WFvLRwH3btfiT9VlKaW+JvIvU9RuNKhMihNY4PXV1uJfv\n" + + "kKsarMDlRgeRMUHJitwCQP3CSiT+ATCmfHz5e83qsJjBPC0d8qc1H+WKYZ2TPvWO\n" + + "egzFLTK73ruhTxGeotr4j6fldriewa/S8R9RHWu+6S3NJ9LNWnt9zUJ85d+f0wY3\n" + + "GVub3y20Zh1dm8A+hnNvK5EB5JyIEP8SFH2N9Cs2YQJn8X7aWYRuBq4KryQDb20n\n" + + "l4FAiRk414D2Z7XKDvxO0sW6AclnT0DfBm4jZDWquY8U5QsAOtvmMhHlZYVlGm8s\n" + + "caqoTx9xMugVzkdWv496nx9kFpMWaNB4KBi5B8MBXOeZchOEFIujH0jeWOXUWgJt\n" + + "hWfNMJSliYlS6VO9aM3ab5SAPcPiHmCkuXXtWBWtmUyUkbWCrZdgq7b4UfGiwQeI\n" + + "q584RnwPOnRpUfglalP1UqufbJMyl7CFjEMVkcxhApp/zgFZZj0w8oeh9aGflcYJ\n" + + "PDvsFoJV0P+VbHlI3FTIg+tJZ73gT/X54Mj5ifUpIZQ/abXSSsgrgnZ4qAjLf8Om\n" + + "GOly5ITEfxJC5rir1yLyBM4T8YJpra3A+3VJo7x/ZatiOxs40uBB4zILIjs5LlCe\n" + + "WAhFzGzq+VvV7LD6c03USxuV70LhfCUH6ZRq4iXFSnjOoWr5tvWZgzVAc7fshlad\n" + + "XZB6lz03jWgNvY66kJK5O6pJ8dftuyihHFY7e44+gQttb+41cYhDmm0Nxxq4PDKW\n" + + "CvI2ETpnW24792D+ZI7XMEfZhY2LoXGYvCkGt5aeo/dsWHoKa3yDjp5/rc2llEFz\n" + + "A3P8mznBfaRNVjW/UhpMAUI3/kn2bbw21ogrm0NuwZGWIS5ea7+G8TjbrznIQsTq\n" + + "VlLhMc7d6gK3hKdDsplX5J90YLA0l1SbQGHqb6GXOsIO2tSRpZWUQIIinYdMDmBG\n" + + "b1wPdwtXmCtyqJfGs/vwmoZdZ0FnwmcsF+bI7LSUnZMK/Cno/Tcl6kWJtvLtG2eC\n" + + "pHxD/tsU3DoArpDa/+/DOotq+u0CB6ymGAi/NnkFKUdNs8oEt0eOw27/F1teKSgv\n" + + "wF4KEcbrHoeSlk/95rtnJYT4IkNA1GSZgYALAMSO2sv7XeBab/jRqM7hyMmzKb3R\n" + + "uXN+BcDHRA1vdvIEpnTD5/EDon3/mr7xgHctzuK8z30aruQoBHWckIgmibB5LNvV\n" + + "xvFFPFkke6dxEXbYWwYwrqUSHk74420euGa58jnuXtQIr0X+g+UTJegzOjt96ZJH\n" + + "l92AHadooL7jYiPX8qxw1sln7k0H+RfWSvEbZ0/xsQ0lxgYwds/Ck6yhOUK8hyRW\n" + + "OVmz3g1QjdwZUDblypsymO3iFggJ0NNhNlYPKEWmwdfTOMDmtuQS97ewDSv0WgAa\n" + + "oUx2FjjM4iOKiyKsM5i8a4ju3MziFu1ghOfixBwtHRbQHneF5/E5cFtrYvuOlAvN\n" + + "80r89YesbBzXzsvheez+bIhm4lTHvBKgcb/RNaseYz/72HVk24GGnisSuc37v+O4\n" + + "YcLflfi86KuLtYQNtR+QyegfYWYogjbsSocWBEfnPJBgtzAtdAnMkaKWbb6WfT4k\n" + + "J6KWH/wANNdjE4yXPJhRevn3PqHnQvKHJqef1DZgzQMcXD3BwOPXxzy1GXXJw4Jn\n" + + "Ma1izl7a+KdbPonCnT59Kg24sl6gJplJRZop/tBqUR/c08kIuEuOB1D+qkeAIv6A\n" + + "3/uK7l4PvVe7XSjZ12Rfm2S7cY4dQybgW81TWKfCDNNXjSAWGAKtfIO7iojzBTF0\n" + + "MPfpuAx0sP++qUXZGsxIOKUhlqZpDNboHw89UDjj8txc9p6NbWTy6VJoYTKv07sG\n" + + "4Umrl5oaX49Ub0GlnwWg/wweCrMXszvZAN58qG0Qt2sjnHy1tUIJ7OajDpWrAEYt\n" + + "cvGzFvsr/j2k9lXBrgtIfSIWo8oQhXDR1gsBw5AxnCWkX0gQPEjYv+rq5zHxfWrF\n" + + "IOG3zXyoO8QHU0TwdA3s7XBd1pbtyaX0BksW7ecqa+J2KkbXhUOQwMTpgCIGkcBV\n" + + "CWf3w6voe6ZPfz4KPR3Zbs9ypV6nbfKjUjjfq7Lms1kOVJqZlJp5hf+ew6hxETHp\n" + + "0QmdhONHZvl+25z4rOquuBwsBXvFw/V5dlvuusi9VBuTUwh/v9JARSNmql8V054M\n" + + "o6Strj5Ukn+ejymZqXs9yeA+cgE3FL4hzdrUEUt8IVLxvD/XYuWROQJ7AckmU9GA\n" + + "xpQxbGcDMV6JzkDihKhiX3D6poccaaaFYv85NNCncsDJrPHrU48PQ4qOyr2sFQa+\n" + + "sfLYfRv5W60Zj3OyVFlK2JrqCu5sT7tecoxCGPCR0m/IpQYYu99JxN2SFv2vV9HI\n" + + "R6Vg18KxWerJ4sWGDe1CKeCCARiBGD8eNajf6JRu+K9VWUjmYpiEkK68Xaa4/Q2T\n" + + "x12WVuyITVU3fCfHp6/0A6wPtJezCvoodqPlw/3fd5eSVYzb5C3v564uhz4=\n" + + "=JP9T\n" + + "-----END PGP MESSAGE-----"; + PGPSecretKeyRing secretKeys = PGPainless.readKeyRing().secretKeyRing(KEY); + PGPPublicKeyRing certificate = PGPainless.extractCertificate(secretKeys); + + Tuple result = processor.process(MSG, ConsumerOptions.get() + .addVerificationCert(certificate) + .addDecryptionKey(secretKeys)); + String plain = result.getA(); + assertEquals("encrypt ∘ sign ∘ sign ∘ sign", plain); + MessageMetadata metadata = result.getB(); + assertEquals(SymmetricKeyAlgorithm.AES_256, metadata.getEncryptionAlgorithm()); + assertNull(metadata.getCompressionAlgorithm()); + } + private static Tuple processReadBuffered(String armoredMessage, ConsumerOptions options) throws PGPException, IOException { OpenPgpMessageInputStream in = get(armoredMessage, options); diff --git a/version.gradle b/version.gradle index dc50f086..7d25632e 100644 --- a/version.gradle +++ b/version.gradle @@ -4,7 +4,7 @@ allprojects { ext { - shortVersion = '1.3.14' + shortVersion = '1.4.0' isSnapshot = true pgpainlessMinAndroidSdk = 10 javaSourceCompatibility = 1.8 From e420678076fb33bad2fd94e6928d760098786115 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 6 Oct 2022 21:52:23 +0200 Subject: [PATCH 037/763] 2/3 the way to working sig verification --- .../DelayedTeeInputStreamInputStream.java | 38 +++ .../OpenPgpMessageInputStream.java | 263 ++++++++++++------ .../TeeBCPGInputStream.java | 36 --- .../OpenPgpMessageInputStreamTest.java | 234 ++++++++++------ .../TeeBCPGInputStreamTest.java | 61 ---- 5 files changed, 353 insertions(+), 279 deletions(-) create mode 100644 pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DelayedTeeInputStreamInputStream.java delete mode 100644 pgpainless-core/src/main/java/org/pgpainless/decryption_verification/TeeBCPGInputStream.java delete mode 100644 pgpainless-core/src/test/java/org/pgpainless/decryption_verification/TeeBCPGInputStreamTest.java diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DelayedTeeInputStreamInputStream.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DelayedTeeInputStreamInputStream.java new file mode 100644 index 00000000..5cbabf89 --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DelayedTeeInputStreamInputStream.java @@ -0,0 +1,38 @@ +package org.pgpainless.decryption_verification; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +public class DelayedTeeInputStreamInputStream extends InputStream { + + private int last = -1; + private final InputStream inputStream; + private final OutputStream outputStream; + + public DelayedTeeInputStreamInputStream(InputStream inputStream, OutputStream outputStream) { + this.inputStream = inputStream; + this.outputStream = outputStream; + } + + @Override + public int read() throws IOException { + if (last != -1) { + outputStream.write(last); + } + last = inputStream.read(); + return last; + } + + /** + * Squeeze the last byte out and update the output stream. + * + * @throws IOException in case of an IO error + */ + public void squeeze() throws IOException { + if (last != -1) { + outputStream.write(last); + } + last = -1; + } +} diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java index 2d031edc..8d370965 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java @@ -20,7 +20,6 @@ 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.PGPPBEEncryptedData; import org.bouncycastle.openpgp.PGPPrivateKey; @@ -53,7 +52,6 @@ import org.pgpainless.key.info.KeyRingInfo; import org.pgpainless.key.protection.SecretKeyRingProtector; import org.pgpainless.key.protection.UnlockSecretKey; import org.pgpainless.signature.SignatureUtils; -import org.pgpainless.util.ArmorUtils; import org.pgpainless.util.Passphrase; import org.pgpainless.util.Tuple; import org.slf4j.Logger; @@ -68,6 +66,7 @@ public class OpenPgpMessageInputStream extends InputStream { protected final OpenPgpMetadata.Builder resultBuilder; // Pushdown Automaton to verify validity of OpenPGP packet sequence in an OpenPGP message protected final PDA automaton = new PDA(); + protected final DelayedTeeInputStreamInputStream delayedTee; // InputStream of OpenPGP packets of the current layer protected final BCPGInputStream packetInputStream; // InputStream of a nested data packet @@ -96,8 +95,9 @@ public class OpenPgpMessageInputStream extends InputStream { this.signatures.addDetachedSignatures(options.getDetachedSignatures()); } - BCPGInputStream bcpg = BCPGInputStream.wrap(inputStream); - this.packetInputStream = new TeeBCPGInputStream(bcpg, signatures); + delayedTee = new DelayedTeeInputStreamInputStream(inputStream, signatures); + BCPGInputStream bcpg = BCPGInputStream.wrap(delayedTee); + this.packetInputStream = bcpg; // *omnomnom* consumePackets(); @@ -121,22 +121,15 @@ public class OpenPgpMessageInputStream extends InputStream { */ private void consumePackets() throws IOException, PGPException { - int tag; - loop: while ((tag = nextTag()) != -1) { - OpenPgpPacket nextPacket; - try { - nextPacket = OpenPgpPacket.requireFromTag(tag); - } catch (NoSuchElementException e) { - log("Invalid tag: " + tag); - throw e; - } - log(nextPacket.toString()); + OpenPgpPacket nextPacket; + loop: while ((nextPacket = nextPacketTag()) != null) { signatures.nextPacket(nextPacket); switch (nextPacket) { // Literal Data - the literal data content is the new input stream case LIT: automaton.next(InputAlphabet.LiteralData); + delayedTee.squeeze(); processLiteralData(); break loop; @@ -145,12 +138,14 @@ public class OpenPgpMessageInputStream extends InputStream { automaton.next(InputAlphabet.CompressedData); signatures.commitNested(); processCompressedData(); + delayedTee.squeeze(); break loop; // One Pass Signature case OPS: automaton.next(InputAlphabet.OnePassSignatures); PGPOnePassSignature onePassSignature = readOnePassSignature(); + delayedTee.squeeze(); signatures.addOnePassSignature(onePassSignature); break; @@ -160,6 +155,7 @@ public class OpenPgpMessageInputStream extends InputStream { automaton.next(InputAlphabet.Signatures); PGPSignature signature = readSignature(); + delayedTee.squeeze(); processSignature(signature, isSigForOPS); break; @@ -170,6 +166,7 @@ public class OpenPgpMessageInputStream extends InputStream { case SED: case SEIPD: automaton.next(InputAlphabet.EncryptedData); + delayedTee.squeeze(); if (processEncryptedData()) { break loop; } @@ -179,6 +176,7 @@ public class OpenPgpMessageInputStream extends InputStream { // Marker Packets need to be skipped and ignored case MARKER: packetInputStream.readPacket(); // skip + delayedTee.squeeze(); break; // Key Packets are illegal in this context @@ -206,6 +204,23 @@ public class OpenPgpMessageInputStream extends InputStream { } } + private OpenPgpPacket nextPacketTag() throws IOException { + int tag = nextTag(); + if (tag == -1) { + log("EOF"); + return null; + } + OpenPgpPacket packet; + try { + packet = OpenPgpPacket.requireFromTag(tag); + } catch (NoSuchElementException e) { + log("Invalid tag: " + tag); + throw e; + } + log(packet.toString()); + return packet; + } + private void processSignature(PGPSignature signature, boolean isSigForOPS) { if (isSigForOPS) { signatures.popNested(); @@ -229,41 +244,6 @@ public class OpenPgpMessageInputStream extends InputStream { nestedInputStream = literalData.getDataStream(); } - private void debugEncryptedData() throws PGPException, IOException { - PGPEncryptedDataList encDataList = new PGPEncryptedDataList(packetInputStream); - - // TODO: Replace with !encDataList.isIntegrityProtected() - if (!encDataList.get(0).isIntegrityProtected()) { - throw new MessageNotIntegrityProtectedException(); - } - - SortedESKs esks = new SortedESKs(encDataList); - for (PGPPublicKeyEncryptedData pkesk : esks.pkesks) { - long keyId = pkesk.getKeyID(); - PGPSecretKeyRing decryptionKeys = getDecryptionKey(keyId); - if (decryptionKeys == null) { - continue; - } - SecretKeyRingProtector protector = options.getSecretKeyProtector(decryptionKeys); - PGPSecretKey decryptionKey = decryptionKeys.getSecretKey(keyId); - PGPPrivateKey privateKey = UnlockSecretKey.unlockSecretKey(decryptionKey, protector); - - PublicKeyDataDecryptorFactory decryptorFactory = ImplementationFactory.getInstance() - .getPublicKeyDataDecryptorFactory(privateKey); - try { - InputStream decrypted = pkesk.getDataStream(decryptorFactory); - InputStream decoder = PGPUtil.getDecoderStream(decrypted); - PGPObjectFactory objectFactory = ImplementationFactory.getInstance() - .getPGPObjectFactory(decoder); - objectFactory.nextObject(); - objectFactory.nextObject(); - objectFactory.nextObject(); - } catch (PGPException e) { - // hm :/ - } - } - } - private boolean processEncryptedData() throws IOException, PGPException { PGPEncryptedDataList encDataList = new PGPEncryptedDataList(packetInputStream); @@ -553,12 +533,13 @@ public class OpenPgpMessageInputStream extends InputStream { // for literal data. UUUUUGLY!!!! private static final class Signatures extends OutputStream { final ConsumerOptions options; - final List detachedSignatures; - final List prependedSignatures; + final List detachedSignatures; + final List prependedSignatures; final List onePassSignatures; final Stack> opsUpdateStack; List literalOPS = new ArrayList<>(); final List correspondingSignatures; + boolean isLiteral = true; private Signatures(ConsumerOptions options) { this.options = options; @@ -579,14 +560,14 @@ public class OpenPgpMessageInputStream extends InputStream { long keyId = SignatureUtils.determineIssuerKeyId(signature); PGPPublicKeyRing certificate = findCertificate(keyId); initialize(signature, certificate, keyId); - this.detachedSignatures.add(signature); + this.detachedSignatures.add(new SIG(signature)); } void addPrependedSignature(PGPSignature signature) { long keyId = SignatureUtils.determineIssuerKeyId(signature); PGPPublicKeyRing certificate = findCertificate(keyId); initialize(signature, certificate, keyId); - this.prependedSignatures.add(signature); + this.prependedSignatures.add(new SIG(signature)); } void addOnePassSignature(PGPOnePassSignature signature) { @@ -630,7 +611,7 @@ public class OpenPgpMessageInputStream extends InputStream { opsUpdateStack.pop(); } - private void initialize(PGPSignature signature, PGPPublicKeyRing certificate, long keyId) { + private static void initialize(PGPSignature signature, PGPPublicKeyRing certificate, long keyId) { if (certificate == null) { // SHIT return; @@ -672,7 +653,7 @@ public class OpenPgpMessageInputStream extends InputStream { ops.update(b); } - for (PGPSignature detached : detachedSignatures) { + for (SIG detached : detachedSignatures) { detached.update(b); } } @@ -682,13 +663,22 @@ public class OpenPgpMessageInputStream extends InputStream { ops.update(b, off, len); } - for (PGPSignature detached : detachedSignatures) { + for (SIG detached : detachedSignatures) { detached.update(b, off, len); } } public void updatePacket(byte b) { - for (List nestedOPSs : opsUpdateStack) { + for (SIG detached : detachedSignatures) { + detached.update(b); + } + + for (SIG prepended : prependedSignatures) { + prepended.update(b); + } + + for (int i = opsUpdateStack.size() - 1; i >= 0; i--) { + List nestedOPSs = opsUpdateStack.get(i); for (OPS ops : nestedOPSs) { ops.update(b); } @@ -696,6 +686,14 @@ public class OpenPgpMessageInputStream extends InputStream { } public void updatePacket(byte[] buf, int off, int len) { + for (SIG detached : detachedSignatures) { + detached.update(buf, off, len); + } + + for (SIG prepended : prependedSignatures) { + prepended.update(buf, off, len); + } + for (int i = opsUpdateStack.size() - 1; i >= 0; i--) { List nestedOPSs = opsUpdateStack.get(i); for (OPS ops : nestedOPSs) { @@ -705,24 +703,16 @@ public class OpenPgpMessageInputStream extends InputStream { } public void finish() { - for (PGPSignature detached : detachedSignatures) { - boolean verified = false; - try { - verified = detached.verify(); - } catch (PGPException e) { - log("Cannot verify detached signature.", e); - } - log("Detached Signature by " + Long.toHexString(detached.getKeyID()) + " is " + (verified ? "verified" : "unverified")); + for (SIG detached : detachedSignatures) { + boolean verified = detached.verify(); + log("Detached Signature by " + Long.toHexString(detached.signature.getKeyID()) + " is " + (verified ? "verified" : "unverified")); + System.out.println(detached); } - for (PGPSignature prepended : prependedSignatures) { - boolean verified = false; - try { - verified = prepended.verify(); - } catch (PGPException e) { - log("Cannot verify prepended signature.", e); - } - log("Prepended Signature by " + Long.toHexString(prepended.getKeyID()) + " is " + (verified ? "verified" : "unverified")); + for (SIG prepended : prependedSignatures) { + boolean verified = prepended.verify(); + log("Prepended Signature by " + Long.toHexString(prepended.signature.getKeyID()) + " is " + (verified ? "verified" : "unverified")); + System.out.println(prepended); } } @@ -738,9 +728,92 @@ public class OpenPgpMessageInputStream extends InputStream { public void nextPacket(OpenPgpPacket nextPacket) { if (nextPacket == OpenPgpPacket.LIT) { + isLiteral = true; if (literalOPS.isEmpty() && !opsUpdateStack.isEmpty()) { literalOPS = opsUpdateStack.pop(); } + } else { + isLiteral = false; + } + } + + static class SIG { + ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + PGPSignature signature; + boolean finished; + boolean valid; + + public SIG(PGPSignature signature) { + this.signature = signature; + } + + public void init(PGPPublicKeyRing certificate) { + initialize(signature, certificate, signature.getKeyID()); + } + + public boolean verify() { + finished = true; + try { + valid = this.signature.verify(); + } catch (PGPException e) { + log("Cannot verify SIG " + signature.getKeyID()); + } + return valid; + } + + public void update(byte b) { + if (finished) { + log("Updating finished sig!"); + return; + } + signature.update(b); + bytes.write(b); + } + + public void update(byte[] bytes, int off, int len) { + if (finished) { + log("Updating finished sig!"); + return; + } + signature.update(bytes, off, len); + this.bytes.write(bytes, off, len); + } + + @Override + public String toString() { + String OPS = "c40d03000a01fbfcc82a015e733001"; + String LIT_H = "cb28620000000000"; + String LIT = "656e637279707420e28898207369676e20e28898207369676e20e28898207369676e"; + String SIG1 = "c2c10400010a006f058262c806350910fbfcc82a015e7330471400000000001e002073616c74406e6f746174696f6e732e736571756f69612d7067702e6f7267b0409ed8ea96dac66447bdff5b7b60c9f80a0ab91d257029153dc3b6d8c27b98162104d1a66e1a23b182c9980f788cfbfcc82a015e7330000029640c00846b5096d92474fd446cc7edaf9f14572cab93a80e12384c1e829f95debc6e8373c2ce5402be53dc1a18cf92a0ed909e0fb38855713ef8ffb13502ffac7c830fa254cc1aa6c666a97b0cc3bc176538f6913d3b8e8981a65cc42df10e0f39e4d0a06dfe961437b59a71892f4fca1116aed15123ea0d86a7b2ce47dd9d3ef22d920631bc011e82babe03ad5d72b3ba7f95bf646f20ccf6f7a4d95de37397c76c7d53741458e51ab6074007f61181c7b88b7c98f5b7510c8dfa3be01f4841501679478b15c5249d928e2a10d15ec63efa1500b994d5bfb32ffb174a976116930eb97a111e6dfd4c5e43e04a5d76ba74806a62fda63a8c3f53f6eebaf852892340e81dd08bbf348454a2cf525aeb512cf33aeeee78465ee4c442e41cc45ac4e3bb0c3333677aa60332ee7f464d9020f8554b82d619872477cca18d8431888f4ae8abe5894e9720f759c410cd7991db12703dc147040dd0d3758223e0b75de6ceae49c1a0c2c45efedeb7114ae785cc886afdc45c82172e4476e1ab5b86dc4314dd76"; + String SIG1f = "c2c13b0400010a006f058262c806350910fbfcc82a015e7330471400000000001e002073616c74406e6f746174696f6e732e736571756f69612d7067702e6f7267b0409ed8ea96dac66447bdff5b7b60c9f80a0ab91d257029153dc3b6d8c27b98162104d1a66e1a23b182c9980f788cfbfcc82a015e7330000029640c00846b5096d92474fd446cc7edaf9f14572cab93a80e12384c1e829f95debc6e8373c2ce5402be53dc1a18cf92a0ed909e0fb38855713ef8ffb13502ffac7c830fa254cc1aa6c666a97b0cc3bc176538f6913d3b8e8981a65cc42df10e0f39e4d0a06dfe961437b59a71892f4fca1116aed15123ea0d86a7b2ce47dd9d3ef22d920631bc011e82babe03ad5d72b3ba7f95bf646f20ccf6f7a4d95de37397c76c7d53741458e51ab6074007f61181c7b88b7c98f5b7510c8dfa3be01f4841501679478b15c5249d928e2a10d15ec63efa1500b994d5bfb32ffb174a976116930eb97a111e6dfd4c5e43e04a5d76ba74806a62fda63a8c3f53f6eebaf852892340e81dd08bbf348454a2cf525aeb512cf33aeeee78465ee4c442e41cc45ac4e3bb0c3333677aa60332ee7f464d9020f8554b82d619872477cca18d8431888f4ae8abe5894e9720f759c410cd7991db12703dc147040dd0d3758223e0b75de6ceae49c1a0c2c45efedeb7114ae785cc886afdc45c82172e4476e1ab5b86dc4314dd76"; + String SIG2 = "c2c10400010a006f058262c806350910fbfcc82a015e7330471400000000001e002073616c74406e6f746174696f6e732e736571756f69612d7067702e6f7267a4d9c117dc7ba3a7e9270856f128d2ab271743eac3cb5750b22a89bd5fd60753162104d1a66e1a23b182c9980f788cfbfcc82a015e73300000b8400bff796c20fa8b25ff7a42686338e06417a2966e85a0fc2723c928bef6cd19d34cf5e7d55ada33080613012dadb79e0278e59d9e7ed7d2d6102912a5f768c2e75b60099225c3d8bfe0c123240188b80dbee89b9b3bd5b13ccc662abc37e2129b6968adac9aba43aa778c0fe4fe337591ee87a96a29a013debc83555293c877144fc676aa1b03782c501949521a320adf6ad96c4f2e036b52a18369c637fdc49033696a84d03a69580b953187fce5aca6fb26fc8815da9f3b513bfe8e304f33ecb4b521aeb7d09c4a284ea66123bd0d6a358b2526d762ca110e1f7f20b3038d774b64d5cfd34e2213765828359d7afc5bf24d5270e99d80c3c1568fa01624b6ea1e9ce4e6890ce9bacf6611a45d41e2671f68f5b096446bf08d27ce75608425b2e3ab92146229ad1fcd8224aca5b5f73960506e7df07bfbf3664348e8ecbfb2eb467b9cfe412cb377a6ee2eb5fd11be9cf9208fe9a74c296f52cfa02a1eb0519ad9a8349bf6ccd6495feb7e391451bf96e08a0798883dee5974e47cbf3b51f111b6d3"; + String SIG2f = "c2c13b0400010a006f058262c806350910fbfcc82a015e7330471400000000001e002073616c74406e6f746174696f6e732e736571756f69612d7067702e6f7267a4d9c117dc7ba3a7e9270856f128d2ab271743eac3cb5750b22a89bd5fd60753162104d1a66e1a23b182c9980f788cfbfcc82a015e73300000b8400bff796c20fa8b25ff7a42686338e06417a2966e85a0fc2723c928bef6cd19d34cf5e7d55ada33080613012dadb79e0278e59d9e7ed7d2d6102912a5f768c2e75b60099225c3d8bfe0c123240188b80dbee89b9b3bd5b13ccc662abc37e2129b6968adac9aba43aa778c0fe4fe337591ee87a96a29a013debc83555293c877144fc676aa1b03782c501949521a320adf6ad96c4f2e036b52a18369c637fdc49033696a84d03a69580b953187fce5aca6fb26fc8815da9f3b513bfe8e304f33ecb4b521aeb7d09c4a284ea66123bd0d6a358b2526d762ca110e1f7f20b3038d774b64d5cfd34e2213765828359d7afc5bf24d5270e99d80c3c1568fa01624b6ea1e9ce4e6890ce9bacf6611a45d41e2671f68f5b096446bf08d27ce75608425b2e3ab92146229ad1fcd8224aca5b5f73960506e7df07bfbf3664348e8ecbfb2eb467b9cfe412cb377a6ee2eb5fd11be9cf9208fe9a74c296f52cfa02a1eb0519ad9a8349bf6ccd6495feb7e391451bf96e08a0798883dee5974e47cbf3b51f111b6d3"; + String out = ""; + + String hex = Hex.toHexString(bytes.toByteArray()); + while (hex.contains(OPS)) { + hex = hex.replace(OPS, "[OPS]"); + } + while (hex.contains(LIT_H)) { + hex = hex.replace(LIT_H, "[LIT]"); + } + while (hex.contains(LIT)) { + hex = hex.replace(LIT, ""); + } + while (hex.contains(SIG1)) { + hex = hex.replace(SIG1, "[SIG1]"); + } + while (hex.contains(SIG1f)) { + hex = hex.replace(SIG1f, "[SIG1f]"); + } + while (hex.contains(SIG2)) { + hex = hex.replace(SIG2, "[SIG2]"); + } + while (hex.contains(SIG2f)) { + hex = hex.replace(SIG2f, "[SIG2f]"); + } + + return out + hex; } } @@ -796,27 +869,35 @@ public class OpenPgpMessageInputStream extends InputStream { String LIT_H = "cb28620000000000"; String LIT = "656e637279707420e28898207369676e20e28898207369676e20e28898207369676e"; String SIG1 = "c2c10400010a006f058262c806350910fbfcc82a015e7330471400000000001e002073616c74406e6f746174696f6e732e736571756f69612d7067702e6f7267b0409ed8ea96dac66447bdff5b7b60c9f80a0ab91d257029153dc3b6d8c27b98162104d1a66e1a23b182c9980f788cfbfcc82a015e7330000029640c00846b5096d92474fd446cc7edaf9f14572cab93a80e12384c1e829f95debc6e8373c2ce5402be53dc1a18cf92a0ed909e0fb38855713ef8ffb13502ffac7c830fa254cc1aa6c666a97b0cc3bc176538f6913d3b8e8981a65cc42df10e0f39e4d0a06dfe961437b59a71892f4fca1116aed15123ea0d86a7b2ce47dd9d3ef22d920631bc011e82babe03ad5d72b3ba7f95bf646f20ccf6f7a4d95de37397c76c7d53741458e51ab6074007f61181c7b88b7c98f5b7510c8dfa3be01f4841501679478b15c5249d928e2a10d15ec63efa1500b994d5bfb32ffb174a976116930eb97a111e6dfd4c5e43e04a5d76ba74806a62fda63a8c3f53f6eebaf852892340e81dd08bbf348454a2cf525aeb512cf33aeeee78465ee4c442e41cc45ac4e3bb0c3333677aa60332ee7f464d9020f8554b82d619872477cca18d8431888f4ae8abe5894e9720f759c410cd7991db12703dc147040dd0d3758223e0b75de6ceae49c1a0c2c45efedeb7114ae785cc886afdc45c82172e4476e1ab5b86dc4314dd76"; + String SIG1f = "c2c13b0400010a006f058262c806350910fbfcc82a015e7330471400000000001e002073616c74406e6f746174696f6e732e736571756f69612d7067702e6f7267b0409ed8ea96dac66447bdff5b7b60c9f80a0ab91d257029153dc3b6d8c27b98162104d1a66e1a23b182c9980f788cfbfcc82a015e7330000029640c00846b5096d92474fd446cc7edaf9f14572cab93a80e12384c1e829f95debc6e8373c2ce5402be53dc1a18cf92a0ed909e0fb38855713ef8ffb13502ffac7c830fa254cc1aa6c666a97b0cc3bc176538f6913d3b8e8981a65cc42df10e0f39e4d0a06dfe961437b59a71892f4fca1116aed15123ea0d86a7b2ce47dd9d3ef22d920631bc011e82babe03ad5d72b3ba7f95bf646f20ccf6f7a4d95de37397c76c7d53741458e51ab6074007f61181c7b88b7c98f5b7510c8dfa3be01f4841501679478b15c5249d928e2a10d15ec63efa1500b994d5bfb32ffb174a976116930eb97a111e6dfd4c5e43e04a5d76ba74806a62fda63a8c3f53f6eebaf852892340e81dd08bbf348454a2cf525aeb512cf33aeeee78465ee4c442e41cc45ac4e3bb0c3333677aa60332ee7f464d9020f8554b82d619872477cca18d8431888f4ae8abe5894e9720f759c410cd7991db12703dc147040dd0d3758223e0b75de6ceae49c1a0c2c45efedeb7114ae785cc886afdc45c82172e4476e1ab5b86dc4314dd76"; String SIG2 = "c2c10400010a006f058262c806350910fbfcc82a015e7330471400000000001e002073616c74406e6f746174696f6e732e736571756f69612d7067702e6f7267a4d9c117dc7ba3a7e9270856f128d2ab271743eac3cb5750b22a89bd5fd60753162104d1a66e1a23b182c9980f788cfbfcc82a015e73300000b8400bff796c20fa8b25ff7a42686338e06417a2966e85a0fc2723c928bef6cd19d34cf5e7d55ada33080613012dadb79e0278e59d9e7ed7d2d6102912a5f768c2e75b60099225c3d8bfe0c123240188b80dbee89b9b3bd5b13ccc662abc37e2129b6968adac9aba43aa778c0fe4fe337591ee87a96a29a013debc83555293c877144fc676aa1b03782c501949521a320adf6ad96c4f2e036b52a18369c637fdc49033696a84d03a69580b953187fce5aca6fb26fc8815da9f3b513bfe8e304f33ecb4b521aeb7d09c4a284ea66123bd0d6a358b2526d762ca110e1f7f20b3038d774b64d5cfd34e2213765828359d7afc5bf24d5270e99d80c3c1568fa01624b6ea1e9ce4e6890ce9bacf6611a45d41e2671f68f5b096446bf08d27ce75608425b2e3ab92146229ad1fcd8224aca5b5f73960506e7df07bfbf3664348e8ecbfb2eb467b9cfe412cb377a6ee2eb5fd11be9cf9208fe9a74c296f52cfa02a1eb0519ad9a8349bf6ccd6495feb7e391451bf96e08a0798883dee5974e47cbf3b51f111b6d3"; - String out = signature.getKeyID() + " last=" + signature.isContaining() + "\n"; + String SIG2f = "c2c13b0400010a006f058262c806350910fbfcc82a015e7330471400000000001e002073616c74406e6f746174696f6e732e736571756f69612d7067702e6f7267a4d9c117dc7ba3a7e9270856f128d2ab271743eac3cb5750b22a89bd5fd60753162104d1a66e1a23b182c9980f788cfbfcc82a015e73300000b8400bff796c20fa8b25ff7a42686338e06417a2966e85a0fc2723c928bef6cd19d34cf5e7d55ada33080613012dadb79e0278e59d9e7ed7d2d6102912a5f768c2e75b60099225c3d8bfe0c123240188b80dbee89b9b3bd5b13ccc662abc37e2129b6968adac9aba43aa778c0fe4fe337591ee87a96a29a013debc83555293c877144fc676aa1b03782c501949521a320adf6ad96c4f2e036b52a18369c637fdc49033696a84d03a69580b953187fce5aca6fb26fc8815da9f3b513bfe8e304f33ecb4b521aeb7d09c4a284ea66123bd0d6a358b2526d762ca110e1f7f20b3038d774b64d5cfd34e2213765828359d7afc5bf24d5270e99d80c3c1568fa01624b6ea1e9ce4e6890ce9bacf6611a45d41e2671f68f5b096446bf08d27ce75608425b2e3ab92146229ad1fcd8224aca5b5f73960506e7df07bfbf3664348e8ecbfb2eb467b9cfe412cb377a6ee2eb5fd11be9cf9208fe9a74c296f52cfa02a1eb0519ad9a8349bf6ccd6495feb7e391451bf96e08a0798883dee5974e47cbf3b51f111b6d3"; + String out = "last=" + signature.isContaining() + "\n"; - String hex = Hex.toHexString(bytes.toByteArray()); - while (hex.contains(OPS)) { - hex = hex.replace(OPS, "[OPS]"); - } - while (hex.contains(LIT_H)) { - hex = hex.replace(LIT_H, "[LIT]"); - } - while (hex.contains(LIT)) { - hex = hex.replace(LIT, ""); - } - while (hex.contains(SIG1)) { - hex = hex.replace(SIG1, "[SIG1]"); - } - while (hex.contains(SIG2)) { - hex = hex.replace(SIG2, "[SIG2]"); - } + String hex = Hex.toHexString(bytes.toByteArray()); + while (hex.contains(OPS)) { + hex = hex.replace(OPS, "[OPS]"); + } + while (hex.contains(LIT_H)) { + hex = hex.replace(LIT_H, "[LIT]"); + } + while (hex.contains(LIT)) { + hex = hex.replace(LIT, ""); + } + while (hex.contains(SIG1)) { + hex = hex.replace(SIG1, "[SIG1]"); + } + while (hex.contains(SIG1f)) { + hex = hex.replace(SIG1f, "[SIG1f]"); + } + while (hex.contains(SIG2)) { + hex = hex.replace(SIG2, "[SIG2]"); + } + while (hex.contains(SIG2f)) { + hex = hex.replace(SIG2f, "[SIG2f]"); + } - return out + hex; + return out + hex; } } } diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/TeeBCPGInputStream.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/TeeBCPGInputStream.java deleted file mode 100644 index cce21051..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/TeeBCPGInputStream.java +++ /dev/null @@ -1,36 +0,0 @@ -package org.pgpainless.decryption_verification; - -import org.bouncycastle.bcpg.BCPGInputStream; -import org.pgpainless.util.ArmorUtils; - -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; - -public class TeeBCPGInputStream extends BCPGInputStream { - - private final OutputStream out; - - public TeeBCPGInputStream(InputStream in, OutputStream outputStream) { - super(in); - this.out = outputStream; - } - - @Override - public int read() throws IOException { - int r = super.read(); - if (r != -1) { - out.write(r); - } - return r; - } - - @Override - public int read(byte[] buf, int off, int len) throws IOException { - int r = super.read(buf, off, len); - if (r > 0) { - out.write(buf, off, r); - } - return r; - } -} diff --git a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStreamTest.java b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStreamTest.java index f780b2eb..46c521ef 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStreamTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStreamTest.java @@ -4,7 +4,6 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.assertTrue; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; @@ -143,11 +142,11 @@ public class OpenPgpMessageInputStreamTest { "-----BEGIN PGP MESSAGE-----\n" + "Version: PGPainless\n" + "\n" + - "hF4Dyqa/GWUy6WsSAQdAQ62BwmUt8Iby0+jvrLhMgST79KR/as+dyl0nf1uki2sw\n" + - "Thg1Ojtf0hOyJgcpQ4nP2Q0wYFR0F1sCydaIlTGreYZHlGtybP7/Ml6KNZILTRWP\n" + - "0kYBkGBgK7oQWRIVyoF2POvEP6EX1X8nvQk7O3NysVdRVbnia7gE3AzRYuha4kxs\n" + - "pI6xJkntLMS3K6him1Y9FHINIASFSB+C\n" + - "=5p00\n" + + "hF4Dyqa/GWUy6WsSAQdAuGt49sQwdAHH3jPx11V3wSh7Amur3TbnONiQYJmMo3Qw\n" + + "87yBnZCsaB7evxLBgi6PpF3tiytHM60xlrPeKKPpJhu60vNafRM2OOwqk7AdcZw4\n" + + "0kYBEhiioO2btSuafNrQEjYzAgC7K6l7aPCcQObNp4ofryXu1P5vN+vUZp357hyS\n" + + "6zZqP+0wJQ9yJZMvFTtFeSaSi0oMP2sb\n" + + "=LvRL\n" + "-----END PGP MESSAGE-----"; public static final String OPS_LIT_SIG = "" + @@ -170,8 +169,8 @@ public class OpenPgpMessageInputStreamTest { // genKey(); // genSIG_LIT(); // genSENC_LIT(); - // genPENC_COMP_LIT(); - genOPS_LIT_SIG(); + genPENC_COMP_LIT(); + // genOPS_LIT_SIG(); } public static void genLIT() throws IOException { @@ -433,91 +432,144 @@ public class OpenPgpMessageInputStreamTest { assertNull(metadata.getCompressionAlgorithm()); } + String BOB_KEY = "-----BEGIN PGP PRIVATE KEY BLOCK-----\n" + + "Comment: Bob's OpenPGP Transferable Secret Key\n" + + "\n" + + "lQVYBF2lnPIBDAC5cL9PQoQLTMuhjbYvb4Ncuuo0bfmgPRFywX53jPhoFf4Zg6mv\n" + + "/seOXpgecTdOcVttfzC8ycIKrt3aQTiwOG/ctaR4Bk/t6ayNFfdUNxHWk4WCKzdz\n" + + "/56fW2O0F23qIRd8UUJp5IIlN4RDdRCtdhVQIAuzvp2oVy/LaS2kxQoKvph/5pQ/\n" + + "5whqsyroEWDJoSV0yOb25B/iwk/pLUFoyhDG9bj0kIzDxrEqW+7Ba8nocQlecMF3\n" + + "X5KMN5kp2zraLv9dlBBpWW43XktjcCZgMy20SouraVma8Je/ECwUWYUiAZxLIlMv\n" + + "9CurEOtxUw6N3RdOtLmYZS9uEnn5y1UkF88o8Nku890uk6BrewFzJyLAx5wRZ4F0\n" + + "qV/yq36UWQ0JB/AUGhHVPdFf6pl6eaxBwT5GXvbBUibtf8YI2og5RsgTWtXfU7eb\n" + + "SGXrl5ZMpbA6mbfhd0R8aPxWfmDWiIOhBufhMCvUHh1sApMKVZnvIff9/0Dca3wb\n" + + "vLIwa3T4CyshfT0AEQEAAQAL/RZqbJW2IqQDCnJi4Ozm++gPqBPiX1RhTWSjwxfM\n" + + "cJKUZfzLj414rMKm6Jh1cwwGY9jekROhB9WmwaaKT8HtcIgrZNAlYzANGRCM4TLK\n" + + "3VskxfSwKKna8l+s+mZglqbAjUg3wmFuf9Tj2xcUZYmyRm1DEmcN2ZzpvRtHgX7z\n" + + "Wn1mAKUlSDJZSQks0zjuMNbupcpyJokdlkUg2+wBznBOTKzgMxVNC9b2g5/tMPUs\n" + + "hGGWmF1UH+7AHMTaS6dlmr2ZBIyogdnfUqdNg5sZwsxSNrbglKP4sqe7X61uEAIQ\n" + + "bD7rT3LonLbhkrj3I8wilUD8usIwt5IecoHhd9HziqZjRCc1BUBkboUEoyedbDV4\n" + + "i4qfsFZ6CEWoLuD5pW7dEp0M+WeuHXO164Rc+LnH6i1VQrpb1Okl4qO6ejIpIjBI\n" + + "1t3GshtUu/mwGBBxs60KBX5g77mFQ9lLCRj8lSYqOsHRKBhUp4qM869VA+fD0BRP\n" + + "fqPT0I9IH4Oa/A3jYJcg622GwQYA1LhnP208Waf6PkQSJ6kyr8ymY1yVh9VBE/g6\n" + + "fRDYA+pkqKnw9wfH2Qho3ysAA+OmVOX8Hldg+Pc0Zs0e5pCavb0En8iFLvTA0Q2E\n" + + "LR5rLue9uD7aFuKFU/VdcddY9Ww/vo4k5p/tVGp7F8RYCFn9rSjIWbfvvZi1q5Tx\n" + + "+akoZbga+4qQ4WYzB/obdX6SCmi6BndcQ1QdjCCQU6gpYx0MddVERbIp9+2SXDyL\n" + + "hpxjSyz+RGsZi/9UAshT4txP4+MZBgDfK3ZqtW+h2/eMRxkANqOJpxSjMyLO/FXN\n" + + "WxzTDYeWtHNYiAlOwlQZEPOydZFty9IVzzNFQCIUCGjQ/nNyhw7adSgUk3+BXEx/\n" + + "MyJPYY0BYuhLxLYcrfQ9nrhaVKxRJj25SVHj2ASsiwGJRZW4CC3uw40OYxfKEvNC\n" + + "mer/VxM3kg8qqGf9KUzJ1dVdAvjyx2Hz6jY2qWCyRQ6IMjWHyd43C4r3jxooYKUC\n" + + "YnstRQyb/gCSKahveSEjo07CiXMr88UGALwzEr3npFAsPW3osGaFLj49y1oRe11E\n" + + "he9gCHFm+fuzbXrWmdPjYU5/ZdqdojzDqfu4ThfnipknpVUM1o6MQqkjM896FHm8\n" + + "zbKVFSMhEP6DPHSCexMFrrSgN03PdwHTO6iBaIBBFqmGY01tmJ03SxvSpiBPON9P\n" + + "NVvy/6UZFedTq8A07OUAxO62YUSNtT5pmK2vzs3SAZJmbFbMh+NN204TRI72GlqT\n" + + "t5hcfkuv8hrmwPS/ZR6q312mKQ6w/1pqO9qitCFCb2IgQmFiYmFnZSA8Ym9iQG9w\n" + + "ZW5wZ3AuZXhhbXBsZT6JAc4EEwEKADgCGwMFCwkIBwIGFQoJCAsCBBYCAwECHgEC\n" + + "F4AWIQTRpm4aI7GCyZgPeIz7/MgqAV5zMAUCXaWe+gAKCRD7/MgqAV5zMG9sC/9U\n" + + "2T3RrqEbw533FPNfEflhEVRIZ8gDXKM8hU6cqqEzCmzZT6xYTe6sv4y+PJBGXJFX\n" + + "yhj0g6FDkSyboM5litOcTupURObVqMgA/Y4UKERznm4fzzH9qek85c4ljtLyNufe\n" + + "doL2pp3vkGtn7eD0QFRaLLmnxPKQ/TlZKdLE1G3u8Uot8QHicaR6GnAdc5UXQJE3\n" + + "BiV7jZuDyWmZ1cUNwJkKL6oRtp+ZNDOQCrLNLecKHcgCqrpjSQG5oouba1I1Q6Vl\n" + + "sP44dhA1nkmLHtxlTOzpeHj4jnk1FaXmyasurrrI5CgU/L2Oi39DGKTH/A/cywDN\n" + + "4ZplIQ9zR8enkbXquUZvFDe+Xz+6xRXtb5MwQyWODB3nHw85HocLwRoIN9WdQEI+\n" + + "L8a/56AuOwhs8llkSuiITjR7r9SgKJC2WlAHl7E8lhJ3VDW3ELC56KH308d6mwOG\n" + + "ZRAqIAKzM1T5FGjMBhq7ZV0eqdEntBh3EcOIfj2M8rg1MzJv+0mHZOIjByawikad\n" + + "BVgEXaWc8gEMANYwv1xsYyunXYK0X1vY/rP1NNPvhLyLIE7NpK90YNBj+xS1ldGD\n" + + "bUdZqZeef2xJe8gMQg05DoD1DF3GipZ0Ies65beh+d5hegb7N4pzh0LzrBrVNHar\n" + + "29b5ExdI7i4iYD5TO6Vr/qTUOiAN/byqELEzAb+L+b2DVz/RoCm4PIp1DU9ewcc2\n" + + "WB38Ofqut3nLYA5tqJ9XvAiEQme+qAVcM3ZFcaMt4I4dXhDZZNg+D9LiTWcxdUPB\n" + + "leu8iwDRjAgyAhPzpFp+nWoqWA81uIiULWD1Fj+IVoY3ZvgivoYOiEFBJ9lbb4te\n" + + "g9m5UT/AaVDTWuHzbspVlbiVe+qyB77C2daWzNyx6UYBPLOo4r0t0c91kbNE5lgj\n" + + "Z7xz6los0N1U8vq91EFSeQJoSQ62XWavYmlCLmdNT6BNfgh4icLsT7Vr1QMX9jzn\n" + + "JtTPxdXytSdHvpSpULsqJ016l0dtmONcK3z9mj5N5z0k1tg1AH970TGYOe2aUcSx\n" + + "IRDMXDOPyzEfjwARAQABAAv9F2CwsjS+Sjh1M1vegJbZjei4gF1HHpEM0K0PSXsp\n" + + "SfVvpR4AoSJ4He6CXSMWg0ot8XKtDuZoV9jnJaES5UL9pMAD7JwIOqZm/DYVJM5h\n" + + "OASCh1c356/wSbFbzRHPtUdZO9Q30WFNJM5pHbCJPjtNoRmRGkf71RxtvHBzy7np\n" + + "Ga+W6U/NVKHw0i0CYwMI0YlKDakYW3Pm+QL+gHZFvngGweTod0f9l2VLLAmeQR/c\n" + + "+EZs7lNumhuZ8mXcwhUc9JQIhOkpO+wreDysEFkAcsKbkQP3UDUsA1gFx9pbMzT0\n" + + "tr1oZq2a4QBtxShHzP/ph7KLpN+6qtjks3xB/yjTgaGmtrwM8tSe0wD1RwXS+/1o\n" + + "BHpXTnQ7TfeOGUAu4KCoOQLv6ELpKWbRBLWuiPwMdbGpvVFALO8+kvKAg9/r+/ny\n" + + "zM2GQHY+J3Jh5JxPiJnHfXNZjIKLbFbIPdSKNyJBuazXW8xIa//mEHMI5OcvsZBK\n" + + "clAIp7LXzjEjKXIwHwDcTn9pBgDpdOKTHOtJ3JUKx0rWVsDH6wq6iKV/FTVSY5jl\n" + + "zN+puOEsskF1Lfxn9JsJihAVO3yNsp6RvkKtyNlFazaCVKtDAmkjoh60XNxcNRqr\n" + + "gCnwdpbgdHP6v/hvZY54ZaJjz6L2e8unNEkYLxDt8cmAyGPgH2XgL7giHIp9jrsQ\n" + + "aS381gnYwNX6wE1aEikgtY91nqJjwPlibF9avSyYQoMtEqM/1UjTjB2KdD/MitK5\n" + + "fP0VpvuXpNYZedmyq4UOMwdkiNMGAOrfmOeT0olgLrTMT5H97Cn3Yxbk13uXHNu/\n" + + "ZUZZNe8s+QtuLfUlKAJtLEUutN33TlWQY522FV0m17S+b80xJib3yZVJteVurrh5\n" + + "HSWHAM+zghQAvCesg5CLXa2dNMkTCmZKgCBvfDLZuZbjFwnwCI6u/NhOY9egKuUf\n" + + "SA/je/RXaT8m5VxLYMxwqQXKApzD87fv0tLPlVIEvjEsaf992tFEFSNPcG1l/jpd\n" + + "5AVXw6kKuf85UkJtYR1x2MkQDrqY1QX/XMw00kt8y9kMZUre19aCArcmor+hDhRJ\n" + + "E3Gt4QJrD9z/bICESw4b4z2DbgD/Xz9IXsA/r9cKiM1h5QMtXvuhyfVeM01enhxM\n" + + "GbOH3gjqqGNKysx0UODGEwr6AV9hAd8RWXMchJLaExK9J5SRawSg671ObAU24SdY\n" + + "vMQ9Z4kAQ2+1ReUZzf3ogSMRZtMT+d18gT6L90/y+APZIaoArLPhebIAGq39HLmJ\n" + + "26x3z0WAgrpA1kNsjXEXkoiZGPLKIGoe3hqJAbYEGAEKACAWIQTRpm4aI7GCyZgP\n" + + "eIz7/MgqAV5zMAUCXaWc8gIbDAAKCRD7/MgqAV5zMOn/C/9ugt+HZIwX308zI+QX\n" + + "c5vDLReuzmJ3ieE0DMO/uNSC+K1XEioSIZP91HeZJ2kbT9nn9fuReuoff0T0Dief\n" + + "rbwcIQQHFFkrqSp1K3VWmUGp2JrUsXFVdjy/fkBIjTd7c5boWljv/6wAsSfiv2V0\n" + + "JSM8EFU6TYXxswGjFVfc6X97tJNeIrXL+mpSmPPqy2bztcCCHkWS5lNLWQw+R7Vg\n" + + "71Fe6yBSNVrqC2/imYG2J9zlowjx1XU63Wdgqp2Wxt0l8OmsB/W80S1fRF5G4SDH\n" + + "s9HXglXXqPsBRZJYfP+VStm9L5P/sKjCcX6WtZR7yS6G8zj/X767MLK/djANvpPd\n" + + "NVniEke6hM3CNBXYPAMhQBMWhCulcoz+0lxi8L34rMN+Dsbma96psdUrn7uLaB91\n" + + "6we0CTfF8qqm7BsVAgalon/UUiuMY80U3ueoj3okiSTiHIjD/YtpXSPioC8nMng7\n" + + "xqAY9Bwizt4FWgXuLm1a4+So4V9j1TRCXd12Uc2l2RNmgDE=\n" + + "=miES\n" + + "-----END PGP PRIVATE KEY BLOCK-----"; + + @ParameterizedTest(name = "Process PENC(OPS OPS LIT SIG SIG) using {0}") + @MethodSource("provideMessageProcessors") + public void testProcessPENC_OPS_OPS_LIT_SIG_SIG(Processor processor) throws IOException, PGPException { + String MSG = "-----BEGIN PGP MESSAGE-----\n" + + "\n" + + "wcDMA3wvqk35PDeyAQv/RhY9sgxMXj1UxumNMOeN+1+c5bB5e3jSrvA93L8yLFqB\n" + + "uF4MsFnHNgu3bS+/a3Z63MRdgS3wOxaRrvEE3y0Q316rP0OQxj9c2mMPZdHlIxjL\n" + + "KJMzQ6Ofs4kdtapo7plFqKBEEvnp7rF1hFAPxi0/Z+ekuhhOnWg6dZpAZH+s5Li0\n" + + "rKUltzFJ0bxPe6LCuwyYnzKnNBJJsQdKwcvX2Ip8+6lTX/DjQR1s5nhIe76GaNcU\n" + + "OvXITOynDsGgNfAmrqTVfrVgDvOVgvj46UPAwS02uYNNk8pWlcy4iGYIlQBUHD6P\n" + + "k1ieG7ETWsJvStceFqLQVgSDErAga/YXXAJnNUF3PnOxgOlVewdxDCoEeu+3OdQE\n" + + "j7hqmTTo3iA5GaTKCOi07NwXoXRhEMN3X6XDI5+ovqzAYaPkITxtqZzoNVKMT5hi\n" + + "tRKl0qwHbMsfHRCQesDmDPU4MlI7TH2iX2jMPxaepyAI++NMW7H6w8bYEFaE0O9v\n" + + "tiTL2gcYv4O/pGd3isWb0sOkAdz7HkKDdFCUdVMwP25z6dwhEy+oR/q1Le1CjCE/\n" + + "kY1bmJCTBmJwf86YGZElxFuvCTUBBX6ChI7+o18fljQE7eIS0GjXkQ1j2zEXxgGy\n" + + "Lhq7yCr6XEIVUj0x8J4LU2RthtgyToOH7EjLRUbqBG2PZD5K7L7b+ueLSkCfM5Gr\n" + + "isGbTTj6e+TLy6rXGxlNmNDoojpfp/5rRCxrmqPOjBZrNcio8rG19PfBkaw1IXu9\n" + + "fV9klsIxQyiOmUIl7sc74tTBwdIq8F6FJ7sJIScSCrzMjy+J+VLaBl1LyKs9cWDr\n" + + "vUqHvc9diwFWjbtZ8wQn9TQug5X4m6sT+pl+7UALAGWdyI9ySlSvVmVnGROKehkV\n" + + "5VfRds1ICH9Y4XAD7ylzF4dJ0gadtgwD97HLmfApP9IFD/sC4Oy2fu/ERky3Qqrw\n" + + "nvxDpFZBAzNiTR5VXlEPH2DeQUL0tyJJtq5InjqJm/F2K6O11Xk/HSm9VP3Bnhbc\n" + + "djaA7GTTYTq2MjPIDYq+ujPkD/WDp5a/2MIWS10ucgZIcLEwJeU/OY+98W/ogrd5\n" + + "tg03XkKLcGuK6sGv1iYsOGw1vI6RKAkI1j7YBXb7Twb3Ueq/lcRvutgMx/O5k0L5\n" + + "+d3kl6XJVQVKneft7C6DEu6boiGQCTtloJFxaJ9POqq6DzTQ5hSGvBNiUuek3HV7\n" + + "lHH544/ONgCufprT3cUSU0CW9EVbeHq3st3wKwxT5ei8nd8R+TuwaPI3TBSqeV03\n" + + "9fz5x9U2a22Uh53/qux2vAl8DyZHw7VWTP/Bu3eWHiDBEQIQY9BbRMYc7ueNwPii\n" + + "EROFOrHikkDr8UPwNC9FmpLd4vmQQfioY1bAuFvDckTrRFRp2ft+8m0oWLuF+3IH\n" + + "lJ2ph3w62VbIOmG0dxtI626n32NcPwk6shCP/gtW1ixuLr1OpiEe5slt2eNiPoTG\n" + + "CX5UnxzwUkyJ9KgLr3uFkMUwITCF9d2HbnHRaYqVDbQBpZW0wmgtpkTp2tNTExvp\n" + + "T2kx8LNHxAYNoSX+OOWvWzimkCO9MUfjpa0i5kVNxHronNcb1hKAU6X/2r2Mt3C4\n" + + "sv2m08spJBQJWnaa/8paYm+c8JS8oACD9SK/8Y4E1kNM3yEgk8dM2BLHKN3xkyT6\n" + + "iPXHKKgEHivTdpDa8gY81uoqorRHt5gNPDqL/p2ttFquBbQUtRvDCMkvqif5DADS\n" + + "wvLnnlOohCnQbFsNtWg5G6UUQ0TYbt6bixHpNcYIuFEJubJOJTuh/paxPgI3xx1q\n" + + "AdrStz97gowgNanOc+Quyt+zmb5cFQdAPLj76xv/W9zd4N601C1NE6+UhZ6mx/Ut\n" + + "wboetRk4HNcTRmBci5gjNoqB5oQnyAyqhHL1yiD3YmwwELnRwE8563HrHEpU6ziq\n" + + "D1pPMF6YBcmSuHp8FubPeef8iGHYEJQscRTIy/sb6YQjgShjE4VXfGJ2vOz3KRfU\n" + + "s7O7MH2b1YkDPsTDuLoDjBzDRoA+2vi034km9Qdcs3w8+vrydw4=\n" + + "=mdYs\n" + + "-----END PGP MESSAGE-----\n"; + PGPSecretKeyRing secretKeys = PGPainless.readKeyRing().secretKeyRing(BOB_KEY); + PGPPublicKeyRing certificate = PGPainless.extractCertificate(secretKeys); + + Tuple result = processor.process(MSG, ConsumerOptions.get() + .addVerificationCert(certificate) + .addDecryptionKey(secretKeys)); + String plain = result.getA(); + assertEquals("encrypt ∘ sign ∘ sign", plain); + MessageMetadata metadata = result.getB(); + assertEquals(SymmetricKeyAlgorithm.AES_256, metadata.getEncryptionAlgorithm()); + assertNull(metadata.getCompressionAlgorithm()); + } + @ParameterizedTest(name = "Process PENC(OPS OPS OPS LIT SIG SIG SIG) using {0}") @MethodSource("provideMessageProcessors") public void testProcessOPS_OPS_OPS_LIT_SIG_SIG_SIG(Processor processor) throws IOException, PGPException { - String KEY = "-----BEGIN PGP PRIVATE KEY BLOCK-----\n" + - "Comment: Bob's OpenPGP Transferable Secret Key\n" + - "\n" + - "lQVYBF2lnPIBDAC5cL9PQoQLTMuhjbYvb4Ncuuo0bfmgPRFywX53jPhoFf4Zg6mv\n" + - "/seOXpgecTdOcVttfzC8ycIKrt3aQTiwOG/ctaR4Bk/t6ayNFfdUNxHWk4WCKzdz\n" + - "/56fW2O0F23qIRd8UUJp5IIlN4RDdRCtdhVQIAuzvp2oVy/LaS2kxQoKvph/5pQ/\n" + - "5whqsyroEWDJoSV0yOb25B/iwk/pLUFoyhDG9bj0kIzDxrEqW+7Ba8nocQlecMF3\n" + - "X5KMN5kp2zraLv9dlBBpWW43XktjcCZgMy20SouraVma8Je/ECwUWYUiAZxLIlMv\n" + - "9CurEOtxUw6N3RdOtLmYZS9uEnn5y1UkF88o8Nku890uk6BrewFzJyLAx5wRZ4F0\n" + - "qV/yq36UWQ0JB/AUGhHVPdFf6pl6eaxBwT5GXvbBUibtf8YI2og5RsgTWtXfU7eb\n" + - "SGXrl5ZMpbA6mbfhd0R8aPxWfmDWiIOhBufhMCvUHh1sApMKVZnvIff9/0Dca3wb\n" + - "vLIwa3T4CyshfT0AEQEAAQAL/RZqbJW2IqQDCnJi4Ozm++gPqBPiX1RhTWSjwxfM\n" + - "cJKUZfzLj414rMKm6Jh1cwwGY9jekROhB9WmwaaKT8HtcIgrZNAlYzANGRCM4TLK\n" + - "3VskxfSwKKna8l+s+mZglqbAjUg3wmFuf9Tj2xcUZYmyRm1DEmcN2ZzpvRtHgX7z\n" + - "Wn1mAKUlSDJZSQks0zjuMNbupcpyJokdlkUg2+wBznBOTKzgMxVNC9b2g5/tMPUs\n" + - "hGGWmF1UH+7AHMTaS6dlmr2ZBIyogdnfUqdNg5sZwsxSNrbglKP4sqe7X61uEAIQ\n" + - "bD7rT3LonLbhkrj3I8wilUD8usIwt5IecoHhd9HziqZjRCc1BUBkboUEoyedbDV4\n" + - "i4qfsFZ6CEWoLuD5pW7dEp0M+WeuHXO164Rc+LnH6i1VQrpb1Okl4qO6ejIpIjBI\n" + - "1t3GshtUu/mwGBBxs60KBX5g77mFQ9lLCRj8lSYqOsHRKBhUp4qM869VA+fD0BRP\n" + - "fqPT0I9IH4Oa/A3jYJcg622GwQYA1LhnP208Waf6PkQSJ6kyr8ymY1yVh9VBE/g6\n" + - "fRDYA+pkqKnw9wfH2Qho3ysAA+OmVOX8Hldg+Pc0Zs0e5pCavb0En8iFLvTA0Q2E\n" + - "LR5rLue9uD7aFuKFU/VdcddY9Ww/vo4k5p/tVGp7F8RYCFn9rSjIWbfvvZi1q5Tx\n" + - "+akoZbga+4qQ4WYzB/obdX6SCmi6BndcQ1QdjCCQU6gpYx0MddVERbIp9+2SXDyL\n" + - "hpxjSyz+RGsZi/9UAshT4txP4+MZBgDfK3ZqtW+h2/eMRxkANqOJpxSjMyLO/FXN\n" + - "WxzTDYeWtHNYiAlOwlQZEPOydZFty9IVzzNFQCIUCGjQ/nNyhw7adSgUk3+BXEx/\n" + - "MyJPYY0BYuhLxLYcrfQ9nrhaVKxRJj25SVHj2ASsiwGJRZW4CC3uw40OYxfKEvNC\n" + - "mer/VxM3kg8qqGf9KUzJ1dVdAvjyx2Hz6jY2qWCyRQ6IMjWHyd43C4r3jxooYKUC\n" + - "YnstRQyb/gCSKahveSEjo07CiXMr88UGALwzEr3npFAsPW3osGaFLj49y1oRe11E\n" + - "he9gCHFm+fuzbXrWmdPjYU5/ZdqdojzDqfu4ThfnipknpVUM1o6MQqkjM896FHm8\n" + - "zbKVFSMhEP6DPHSCexMFrrSgN03PdwHTO6iBaIBBFqmGY01tmJ03SxvSpiBPON9P\n" + - "NVvy/6UZFedTq8A07OUAxO62YUSNtT5pmK2vzs3SAZJmbFbMh+NN204TRI72GlqT\n" + - "t5hcfkuv8hrmwPS/ZR6q312mKQ6w/1pqO9qitCFCb2IgQmFiYmFnZSA8Ym9iQG9w\n" + - "ZW5wZ3AuZXhhbXBsZT6JAc4EEwEKADgCGwMFCwkIBwIGFQoJCAsCBBYCAwECHgEC\n" + - "F4AWIQTRpm4aI7GCyZgPeIz7/MgqAV5zMAUCXaWe+gAKCRD7/MgqAV5zMG9sC/9U\n" + - "2T3RrqEbw533FPNfEflhEVRIZ8gDXKM8hU6cqqEzCmzZT6xYTe6sv4y+PJBGXJFX\n" + - "yhj0g6FDkSyboM5litOcTupURObVqMgA/Y4UKERznm4fzzH9qek85c4ljtLyNufe\n" + - "doL2pp3vkGtn7eD0QFRaLLmnxPKQ/TlZKdLE1G3u8Uot8QHicaR6GnAdc5UXQJE3\n" + - "BiV7jZuDyWmZ1cUNwJkKL6oRtp+ZNDOQCrLNLecKHcgCqrpjSQG5oouba1I1Q6Vl\n" + - "sP44dhA1nkmLHtxlTOzpeHj4jnk1FaXmyasurrrI5CgU/L2Oi39DGKTH/A/cywDN\n" + - "4ZplIQ9zR8enkbXquUZvFDe+Xz+6xRXtb5MwQyWODB3nHw85HocLwRoIN9WdQEI+\n" + - "L8a/56AuOwhs8llkSuiITjR7r9SgKJC2WlAHl7E8lhJ3VDW3ELC56KH308d6mwOG\n" + - "ZRAqIAKzM1T5FGjMBhq7ZV0eqdEntBh3EcOIfj2M8rg1MzJv+0mHZOIjByawikad\n" + - "BVgEXaWc8gEMANYwv1xsYyunXYK0X1vY/rP1NNPvhLyLIE7NpK90YNBj+xS1ldGD\n" + - "bUdZqZeef2xJe8gMQg05DoD1DF3GipZ0Ies65beh+d5hegb7N4pzh0LzrBrVNHar\n" + - "29b5ExdI7i4iYD5TO6Vr/qTUOiAN/byqELEzAb+L+b2DVz/RoCm4PIp1DU9ewcc2\n" + - "WB38Ofqut3nLYA5tqJ9XvAiEQme+qAVcM3ZFcaMt4I4dXhDZZNg+D9LiTWcxdUPB\n" + - "leu8iwDRjAgyAhPzpFp+nWoqWA81uIiULWD1Fj+IVoY3ZvgivoYOiEFBJ9lbb4te\n" + - "g9m5UT/AaVDTWuHzbspVlbiVe+qyB77C2daWzNyx6UYBPLOo4r0t0c91kbNE5lgj\n" + - "Z7xz6los0N1U8vq91EFSeQJoSQ62XWavYmlCLmdNT6BNfgh4icLsT7Vr1QMX9jzn\n" + - "JtTPxdXytSdHvpSpULsqJ016l0dtmONcK3z9mj5N5z0k1tg1AH970TGYOe2aUcSx\n" + - "IRDMXDOPyzEfjwARAQABAAv9F2CwsjS+Sjh1M1vegJbZjei4gF1HHpEM0K0PSXsp\n" + - "SfVvpR4AoSJ4He6CXSMWg0ot8XKtDuZoV9jnJaES5UL9pMAD7JwIOqZm/DYVJM5h\n" + - "OASCh1c356/wSbFbzRHPtUdZO9Q30WFNJM5pHbCJPjtNoRmRGkf71RxtvHBzy7np\n" + - "Ga+W6U/NVKHw0i0CYwMI0YlKDakYW3Pm+QL+gHZFvngGweTod0f9l2VLLAmeQR/c\n" + - "+EZs7lNumhuZ8mXcwhUc9JQIhOkpO+wreDysEFkAcsKbkQP3UDUsA1gFx9pbMzT0\n" + - "tr1oZq2a4QBtxShHzP/ph7KLpN+6qtjks3xB/yjTgaGmtrwM8tSe0wD1RwXS+/1o\n" + - "BHpXTnQ7TfeOGUAu4KCoOQLv6ELpKWbRBLWuiPwMdbGpvVFALO8+kvKAg9/r+/ny\n" + - "zM2GQHY+J3Jh5JxPiJnHfXNZjIKLbFbIPdSKNyJBuazXW8xIa//mEHMI5OcvsZBK\n" + - "clAIp7LXzjEjKXIwHwDcTn9pBgDpdOKTHOtJ3JUKx0rWVsDH6wq6iKV/FTVSY5jl\n" + - "zN+puOEsskF1Lfxn9JsJihAVO3yNsp6RvkKtyNlFazaCVKtDAmkjoh60XNxcNRqr\n" + - "gCnwdpbgdHP6v/hvZY54ZaJjz6L2e8unNEkYLxDt8cmAyGPgH2XgL7giHIp9jrsQ\n" + - "aS381gnYwNX6wE1aEikgtY91nqJjwPlibF9avSyYQoMtEqM/1UjTjB2KdD/MitK5\n" + - "fP0VpvuXpNYZedmyq4UOMwdkiNMGAOrfmOeT0olgLrTMT5H97Cn3Yxbk13uXHNu/\n" + - "ZUZZNe8s+QtuLfUlKAJtLEUutN33TlWQY522FV0m17S+b80xJib3yZVJteVurrh5\n" + - "HSWHAM+zghQAvCesg5CLXa2dNMkTCmZKgCBvfDLZuZbjFwnwCI6u/NhOY9egKuUf\n" + - "SA/je/RXaT8m5VxLYMxwqQXKApzD87fv0tLPlVIEvjEsaf992tFEFSNPcG1l/jpd\n" + - "5AVXw6kKuf85UkJtYR1x2MkQDrqY1QX/XMw00kt8y9kMZUre19aCArcmor+hDhRJ\n" + - "E3Gt4QJrD9z/bICESw4b4z2DbgD/Xz9IXsA/r9cKiM1h5QMtXvuhyfVeM01enhxM\n" + - "GbOH3gjqqGNKysx0UODGEwr6AV9hAd8RWXMchJLaExK9J5SRawSg671ObAU24SdY\n" + - "vMQ9Z4kAQ2+1ReUZzf3ogSMRZtMT+d18gT6L90/y+APZIaoArLPhebIAGq39HLmJ\n" + - "26x3z0WAgrpA1kNsjXEXkoiZGPLKIGoe3hqJAbYEGAEKACAWIQTRpm4aI7GCyZgP\n" + - "eIz7/MgqAV5zMAUCXaWc8gIbDAAKCRD7/MgqAV5zMOn/C/9ugt+HZIwX308zI+QX\n" + - "c5vDLReuzmJ3ieE0DMO/uNSC+K1XEioSIZP91HeZJ2kbT9nn9fuReuoff0T0Dief\n" + - "rbwcIQQHFFkrqSp1K3VWmUGp2JrUsXFVdjy/fkBIjTd7c5boWljv/6wAsSfiv2V0\n" + - "JSM8EFU6TYXxswGjFVfc6X97tJNeIrXL+mpSmPPqy2bztcCCHkWS5lNLWQw+R7Vg\n" + - "71Fe6yBSNVrqC2/imYG2J9zlowjx1XU63Wdgqp2Wxt0l8OmsB/W80S1fRF5G4SDH\n" + - "s9HXglXXqPsBRZJYfP+VStm9L5P/sKjCcX6WtZR7yS6G8zj/X767MLK/djANvpPd\n" + - "NVniEke6hM3CNBXYPAMhQBMWhCulcoz+0lxi8L34rMN+Dsbma96psdUrn7uLaB91\n" + - "6we0CTfF8qqm7BsVAgalon/UUiuMY80U3ueoj3okiSTiHIjD/YtpXSPioC8nMng7\n" + - "xqAY9Bwizt4FWgXuLm1a4+So4V9j1TRCXd12Uc2l2RNmgDE=\n" + - "=miES\n" + - "-----END PGP PRIVATE KEY BLOCK-----"; String MSG = "-----BEGIN PGP MESSAGE-----\n" + "\n" + "wcDMA3wvqk35PDeyAQwA0yaEgydkAMEfl7rDTYVGanLKiFiWIs34mkF+LB8qR5eY\n" + @@ -565,7 +617,7 @@ public class OpenPgpMessageInputStreamTest { "x12WVuyITVU3fCfHp6/0A6wPtJezCvoodqPlw/3fd5eSVYzb5C3v564uhz4=\n" + "=JP9T\n" + "-----END PGP MESSAGE-----"; - PGPSecretKeyRing secretKeys = PGPainless.readKeyRing().secretKeyRing(KEY); + PGPSecretKeyRing secretKeys = PGPainless.readKeyRing().secretKeyRing(BOB_KEY); PGPPublicKeyRing certificate = PGPainless.extractCertificate(secretKeys); Tuple result = processor.process(MSG, ConsumerOptions.get() diff --git a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/TeeBCPGInputStreamTest.java b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/TeeBCPGInputStreamTest.java deleted file mode 100644 index d31c6009..00000000 --- a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/TeeBCPGInputStreamTest.java +++ /dev/null @@ -1,61 +0,0 @@ -package org.pgpainless.decryption_verification; - -import org.bouncycastle.bcpg.ArmoredInputStream; -import org.bouncycastle.bcpg.ArmoredOutputStream; -import org.bouncycastle.bcpg.BCPGInputStream; -import org.bouncycastle.bcpg.Packet; -import org.bouncycastle.openpgp.PGPCompressedData; -import org.bouncycastle.openpgp.PGPException; -import org.junit.jupiter.api.Test; -import org.pgpainless.algorithm.OpenPgpPacket; -import org.pgpainless.util.ArmoredInputStreamFactory; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.nio.charset.StandardCharsets; - -public class TeeBCPGInputStreamTest { - - private static final Logger LOGGER = LoggerFactory.getLogger(TeeBCPGInputStreamTest.class); - private static final String INBAND_SIGNED = "-----BEGIN PGP MESSAGE-----\n" + - "Version: PGPainless\n" + - "\n" + - "owGbwMvMyCUWdXSHvVTUtXbG0yJJDCDgkZqTk6+jEJ5flJOiyNVRysIoxsXAxsqU\n" + - "GDiVjUGRUwCmQUyRRWnOn9Z/PIseF3Yz6cCEL05nZDj1OClo75WVTjNmJPemW6qV\n" + - "6ki//1K1++2s0qTP+0N11O4z/BVLDDdxnmQryS+5VXjBX7/0Hxnm/eqeX6Zum35r\n" + - "M8e7ufwA\n" + - "=RDiy\n" + - "-----END PGP MESSAGE-----"; - - @Test - public void test() throws IOException, PGPException { - ByteArrayOutputStream out = new ByteArrayOutputStream(); - ArmoredOutputStream armorOut = new ArmoredOutputStream(out); - - ByteArrayInputStream bytesIn = new ByteArrayInputStream(INBAND_SIGNED.getBytes(StandardCharsets.UTF_8)); - ArmoredInputStream armorIn = ArmoredInputStreamFactory.get(bytesIn); - BCPGInputStream bcpgIn = new BCPGInputStream(armorIn); - TeeBCPGInputStream teeIn = new TeeBCPGInputStream(bcpgIn, armorOut); - - ByteArrayOutputStream nestedOut = new ByteArrayOutputStream(); - ArmoredOutputStream nestedArmorOut = new ArmoredOutputStream(nestedOut); - - PGPCompressedData compressedData = new PGPCompressedData(teeIn); - InputStream nestedStream = compressedData.getDataStream(); - BCPGInputStream nestedBcpgIn = new BCPGInputStream(nestedStream); - TeeBCPGInputStream nestedTeeIn = new TeeBCPGInputStream(nestedBcpgIn, nestedArmorOut); - - int tag; - while ((tag = nestedTeeIn.nextPacketTag()) != -1) { - LOGGER.debug(OpenPgpPacket.requireFromTag(tag).toString()); - Packet packet = nestedTeeIn.readPacket(); - } - - nestedArmorOut.close(); - LOGGER.debug(nestedOut.toString()); - } -} From bdc968dd430712c80ccbe8aacdbb4411a8a8b9e2 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Sat, 8 Oct 2022 13:57:53 +0200 Subject: [PATCH 038/763] Create TeeBCPGInputStream to move teeing logic out of OpenPgpMessageInputStream --- .../DelayedTeeInputStreamInputStream.java | 38 ----- .../OpenPgpMessageInputStream.java | 107 +++++--------- .../TeeBCPGInputStream.java | 131 ++++++++++++++++++ .../automaton/InputAlphabet.java | 4 +- .../automaton/PDA.java | 18 +-- .../OpenPgpMessageInputStreamTest.java | 3 +- .../automaton/PDATest.java | 10 +- 7 files changed, 182 insertions(+), 129 deletions(-) delete mode 100644 pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DelayedTeeInputStreamInputStream.java create mode 100644 pgpainless-core/src/main/java/org/pgpainless/decryption_verification/TeeBCPGInputStream.java diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DelayedTeeInputStreamInputStream.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DelayedTeeInputStreamInputStream.java deleted file mode 100644 index 5cbabf89..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DelayedTeeInputStreamInputStream.java +++ /dev/null @@ -1,38 +0,0 @@ -package org.pgpainless.decryption_verification; - -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; - -public class DelayedTeeInputStreamInputStream extends InputStream { - - private int last = -1; - private final InputStream inputStream; - private final OutputStream outputStream; - - public DelayedTeeInputStreamInputStream(InputStream inputStream, OutputStream outputStream) { - this.inputStream = inputStream; - this.outputStream = outputStream; - } - - @Override - public int read() throws IOException { - if (last != -1) { - outputStream.write(last); - } - last = inputStream.read(); - return last; - } - - /** - * Squeeze the last byte out and update the output stream. - * - * @throws IOException in case of an IO error - */ - public void squeeze() throws IOException { - if (last != -1) { - outputStream.write(last); - } - last = -1; - } -} diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java index 8d370965..75359de1 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java @@ -11,7 +11,6 @@ import java.io.OutputStream; import java.util.ArrayList; import java.util.Collection; import java.util.List; -import java.util.NoSuchElementException; import java.util.Stack; import org.bouncycastle.bcpg.BCPGInputStream; @@ -57,6 +56,8 @@ import org.pgpainless.util.Tuple; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import javax.annotation.Nonnull; + public class OpenPgpMessageInputStream extends InputStream { private static final Logger LOGGER = LoggerFactory.getLogger(OpenPgpMessageInputStream.class); @@ -66,16 +67,15 @@ public class OpenPgpMessageInputStream extends InputStream { protected final OpenPgpMetadata.Builder resultBuilder; // Pushdown Automaton to verify validity of OpenPGP packet sequence in an OpenPGP message protected final PDA automaton = new PDA(); - protected final DelayedTeeInputStreamInputStream delayedTee; - // InputStream of OpenPGP packets of the current layer - protected final BCPGInputStream packetInputStream; + // InputStream of OpenPGP packets + protected TeeBCPGInputStream packetInputStream; // InputStream of a nested data packet protected InputStream nestedInputStream; private boolean closed = false; private final Signatures signatures; - private MessageMetadata.Layer metadata; + private final MessageMetadata.Layer metadata; public OpenPgpMessageInputStream(InputStream inputStream, ConsumerOptions options) throws IOException, PGPException { @@ -95,9 +95,7 @@ public class OpenPgpMessageInputStream extends InputStream { this.signatures.addDetachedSignatures(options.getDetachedSignatures()); } - delayedTee = new DelayedTeeInputStreamInputStream(inputStream, signatures); - BCPGInputStream bcpg = BCPGInputStream.wrap(delayedTee); - this.packetInputStream = bcpg; + packetInputStream = new TeeBCPGInputStream(BCPGInputStream.wrap(inputStream), signatures); // *omnomnom* consumePackets(); @@ -122,41 +120,36 @@ public class OpenPgpMessageInputStream extends InputStream { private void consumePackets() throws IOException, PGPException { OpenPgpPacket nextPacket; - loop: while ((nextPacket = nextPacketTag()) != null) { + loop: while ((nextPacket = packetInputStream.nextPacketTag()) != null) { signatures.nextPacket(nextPacket); switch (nextPacket) { // Literal Data - the literal data content is the new input stream case LIT: automaton.next(InputAlphabet.LiteralData); - delayedTee.squeeze(); processLiteralData(); break loop; // Compressed Data - the content contains another OpenPGP message case COMP: automaton.next(InputAlphabet.CompressedData); - signatures.commitNested(); processCompressedData(); - delayedTee.squeeze(); break loop; // One Pass Signature case OPS: - automaton.next(InputAlphabet.OnePassSignatures); + automaton.next(InputAlphabet.OnePassSignature); PGPOnePassSignature onePassSignature = readOnePassSignature(); - delayedTee.squeeze(); signatures.addOnePassSignature(onePassSignature); break; // Signature - either prepended to the message, or corresponding to a One Pass Signature case SIG: + // true if Signature corresponds to OnePassSignature boolean isSigForOPS = automaton.peekStack() == StackAlphabet.ops; - automaton.next(InputAlphabet.Signatures); + automaton.next(InputAlphabet.Signature); - PGPSignature signature = readSignature(); - delayedTee.squeeze(); - processSignature(signature, isSigForOPS); + processSignature(isSigForOPS); break; @@ -166,7 +159,6 @@ public class OpenPgpMessageInputStream extends InputStream { case SED: case SEIPD: automaton.next(InputAlphabet.EncryptedData); - delayedTee.squeeze(); if (processEncryptedData()) { break loop; } @@ -175,8 +167,7 @@ public class OpenPgpMessageInputStream extends InputStream { // Marker Packets need to be skipped and ignored case MARKER: - packetInputStream.readPacket(); // skip - delayedTee.squeeze(); + packetInputStream.readMarker(); break; // Key Packets are illegal in this context @@ -204,26 +195,10 @@ public class OpenPgpMessageInputStream extends InputStream { } } - private OpenPgpPacket nextPacketTag() throws IOException { - int tag = nextTag(); - if (tag == -1) { - log("EOF"); - return null; - } - OpenPgpPacket packet; - try { - packet = OpenPgpPacket.requireFromTag(tag); - } catch (NoSuchElementException e) { - log("Invalid tag: " + tag); - throw e; - } - log(packet.toString()); - return packet; - } - - private void processSignature(PGPSignature signature, boolean isSigForOPS) { + private void processSignature(boolean isSigForOPS) throws PGPException, IOException { + PGPSignature signature = readSignature(); if (isSigForOPS) { - signatures.popNested(); + signatures.leaveNesting(); // TODO: Only leave nesting if all OPSs are dealt with signatures.addCorrespondingOnePassSignature(signature); } else { signatures.addPrependedSignature(signature); @@ -231,21 +206,22 @@ public class OpenPgpMessageInputStream extends InputStream { } private void processCompressedData() throws IOException, PGPException { - PGPCompressedData compressedData = new PGPCompressedData(packetInputStream); + signatures.enterNesting(); + PGPCompressedData compressedData = packetInputStream.readCompressedData(); MessageMetadata.CompressedData compressionLayer = new MessageMetadata.CompressedData( CompressionAlgorithm.fromId(compressedData.getAlgorithm())); nestedInputStream = new OpenPgpMessageInputStream(compressedData.getDataStream(), options, compressionLayer); } private void processLiteralData() throws IOException { - PGPLiteralData literalData = new PGPLiteralData(packetInputStream); + PGPLiteralData literalData = packetInputStream.readLiteralData(); this.metadata.setChild(new MessageMetadata.LiteralData(literalData.getFileName(), literalData.getModificationTime(), StreamEncoding.requireFromCode(literalData.getFormat()))); nestedInputStream = literalData.getDataStream(); } private boolean processEncryptedData() throws IOException, PGPException { - PGPEncryptedDataList encDataList = new PGPEncryptedDataList(packetInputStream); + PGPEncryptedDataList encDataList = packetInputStream.readEncryptedDataList(); // TODO: Replace with !encDataList.isIntegrityProtected() if (!encDataList.get(0).isIntegrityProtected()) { @@ -345,19 +321,6 @@ public class OpenPgpMessageInputStream extends InputStream { return false; } - private int nextTag() throws IOException { - try { - return packetInputStream.nextPacketTag(); - } catch (IOException e) { - if ("Stream closed".equals(e.getMessage())) { - // ZipInflater Streams sometimes close under our feet -.- - // Therefore we catch resulting IOEs and return -1 instead. - return -1; - } - throw e; - } - } - private List> findPotentialDecryptionKeys(PGPPublicKeyEncryptedData pkesk) { int algorithm = pkesk.getAlgorithm(); List> decryptionKeyCandidates = new ArrayList<>(); @@ -387,12 +350,12 @@ public class OpenPgpMessageInputStream extends InputStream { private PGPOnePassSignature readOnePassSignature() throws PGPException, IOException { - return new PGPOnePassSignature(packetInputStream); + return packetInputStream.readOnePassSignature(); } private PGPSignature readSignature() throws PGPException, IOException { - return new PGPSignature(packetInputStream); + return packetInputStream.readSignature(); } @Override @@ -428,7 +391,7 @@ public class OpenPgpMessageInputStream extends InputStream { } @Override - public int read(byte[] b, int off, int len) + public int read(@Nonnull byte[] b, int off, int len) throws IOException { if (nestedInputStream == null) { @@ -482,8 +445,7 @@ public class OpenPgpMessageInputStream extends InputStream { private void collectMetadata() { if (nestedInputStream instanceof OpenPgpMessageInputStream) { OpenPgpMessageInputStream child = (OpenPgpMessageInputStream) nestedInputStream; - MessageMetadata.Layer childLayer = child.metadata; - this.metadata.setChild((MessageMetadata.Nested) childLayer); + this.metadata.setChild((MessageMetadata.Nested) child.metadata); } } @@ -496,9 +458,9 @@ public class OpenPgpMessageInputStream extends InputStream { private static class SortedESKs { - private List skesks = new ArrayList<>(); - private List pkesks = new ArrayList<>(); - private List anonPkesks = new ArrayList<>(); + private final List skesks = new ArrayList<>(); + private final List pkesks = new ArrayList<>(); + private final List anonPkesks = new ArrayList<>(); SortedESKs(PGPEncryptedDataList esks) { for (PGPEncryptedData esk : esks) { @@ -526,11 +488,10 @@ public class OpenPgpMessageInputStream extends InputStream { } } - // TODO: In 'OPS LIT("Foo") SIG', OPS is only updated with "Foo" - // In 'OPS[1] OPS LIT("Foo") SIG SIG', OPS[1] (nested) is updated with OPS LIT("Foo") SIG. - // Therefore, we need to handle the innermost signature layer differently when updating with Literal data. - // For this we might want to provide two update entries into the Signatures class, one for OpenPGP packets and one - // for literal data. UUUUUGLY!!!! + // In 'OPS LIT("Foo") SIG', OPS is only updated with "Foo" + // In 'OPS[1] OPS LIT("Foo") SIG SIG', OPS[1] (nested) is updated with OPS LIT("Foo") SIG. + // Therefore, we need to handle the innermost signature layer differently when updating with Literal data. + // Furthermore, For 'OPS COMP(LIT("Foo")) SIG', the signature is updated with "Foo". CHAOS!!! private static final class Signatures extends OutputStream { final ConsumerOptions options; final List detachedSignatures; @@ -578,7 +539,7 @@ public class OpenPgpMessageInputStream extends InputStream { literalOPS.add(ops); if (signature.isContaining()) { - commitNested(); + enterNesting(); } } @@ -599,12 +560,12 @@ public class OpenPgpMessageInputStream extends InputStream { } } - void commitNested() { + void enterNesting() { opsUpdateStack.push(literalOPS); literalOPS = new ArrayList<>(); } - void popNested() { + void leaveNesting() { if (opsUpdateStack.isEmpty()) { return; } @@ -722,7 +683,7 @@ public class OpenPgpMessageInputStream extends InputStream { } @Override - public void write(byte[] b, int off, int len) { + public void write(@Nonnull byte[] b, int off, int len) { updatePacket(b, off, len); } diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/TeeBCPGInputStream.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/TeeBCPGInputStream.java new file mode 100644 index 00000000..f80793f0 --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/TeeBCPGInputStream.java @@ -0,0 +1,131 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.decryption_verification; + +import org.bouncycastle.bcpg.BCPGInputStream; +import org.bouncycastle.bcpg.MarkerPacket; +import org.bouncycastle.bcpg.Packet; +import org.bouncycastle.openpgp.PGPCompressedData; +import org.bouncycastle.openpgp.PGPEncryptedDataList; +import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.PGPLiteralData; +import org.bouncycastle.openpgp.PGPOnePassSignature; +import org.bouncycastle.openpgp.PGPSignature; +import org.pgpainless.algorithm.OpenPgpPacket; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.NoSuchElementException; + +/** + * Since we need to update signatures with data from the underlying stream, this class is used to tee out the data. + * Unfortunately we cannot simply override {@link BCPGInputStream#read()} to tee the data out though, since + * {@link BCPGInputStream#readPacket()} inconsistently calls a mix of {@link BCPGInputStream#read()} and + * {@link InputStream#read()} of the underlying stream. This would cause the second length byte to get swallowed up. + * + * Therefore, this class delegates the teeing to an {@link DelayedTeeInputStreamInputStream} which wraps the underlying + * stream. Since calling {@link BCPGInputStream#nextPacketTag()} reads up to and including the next packets tag, + * we need to delay teeing out that byte to signature verifiers. + * Hence, the reading methods of the {@link TeeBCPGInputStream} handle pushing this byte to the output stream using + * {@link DelayedTeeInputStreamInputStream#squeeze()}. + */ +public class TeeBCPGInputStream { + + protected final DelayedTeeInputStreamInputStream delayedTee; + // InputStream of OpenPGP packets of the current layer + protected final BCPGInputStream packetInputStream; + + public TeeBCPGInputStream(BCPGInputStream inputStream, OutputStream outputStream) { + this.delayedTee = new DelayedTeeInputStreamInputStream(inputStream, outputStream); + this.packetInputStream = BCPGInputStream.wrap(delayedTee); + } + + public OpenPgpPacket nextPacketTag() throws IOException { + int tag = packetInputStream.nextPacketTag(); + if (tag == -1) { + return null; + } + + OpenPgpPacket packet; + try { + packet = OpenPgpPacket.requireFromTag(tag); + } catch (NoSuchElementException e) { + throw e; + } + return packet; + } + + public Packet readPacket() throws IOException { + return packetInputStream.readPacket(); + } + + public PGPCompressedData readCompressedData() throws IOException { + delayedTee.squeeze(); + PGPCompressedData compressedData = new PGPCompressedData(packetInputStream); + return compressedData; + } + + public PGPLiteralData readLiteralData() throws IOException { + delayedTee.squeeze(); + return new PGPLiteralData(packetInputStream); + } + + public PGPEncryptedDataList readEncryptedDataList() throws IOException { + delayedTee.squeeze(); + return new PGPEncryptedDataList(packetInputStream); + } + + public PGPOnePassSignature readOnePassSignature() throws PGPException, IOException { + PGPOnePassSignature onePassSignature = new PGPOnePassSignature(packetInputStream); + delayedTee.squeeze(); + return onePassSignature; + } + + public PGPSignature readSignature() throws PGPException, IOException { + PGPSignature signature = new PGPSignature(packetInputStream); + delayedTee.squeeze(); + return signature; + } + + public MarkerPacket readMarker() throws IOException { + MarkerPacket markerPacket = (MarkerPacket) readPacket(); + delayedTee.squeeze(); + return markerPacket; + } + + public static class DelayedTeeInputStreamInputStream extends InputStream { + + private int last = -1; + private final InputStream inputStream; + private final OutputStream outputStream; + + public DelayedTeeInputStreamInputStream(InputStream inputStream, OutputStream outputStream) { + this.inputStream = inputStream; + this.outputStream = outputStream; + } + + @Override + public int read() throws IOException { + if (last != -1) { + outputStream.write(last); + } + last = inputStream.read(); + return last; + } + + /** + * Squeeze the last byte out and update the output stream. + * + * @throws IOException in case of an IO error + */ + public void squeeze() throws IOException { + if (last != -1) { + outputStream.write(last); + } + last = -1; + } + } +} diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/automaton/InputAlphabet.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/automaton/InputAlphabet.java index 8e795f5b..ad2a8c55 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/automaton/InputAlphabet.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/automaton/InputAlphabet.java @@ -18,11 +18,11 @@ public enum InputAlphabet { /** * A {@link PGPSignatureList} object. */ - Signatures, + Signature, /** * A {@link PGPOnePassSignatureList} object. */ - OnePassSignatures, + OnePassSignature, /** * A {@link PGPCompressedData} packet. * The contents of this packet MUST form a valid OpenPGP message, so a nested PDA is opened to verify diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/automaton/PDA.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/automaton/PDA.java index 1d4b1189..156dc2ed 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/automaton/PDA.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/automaton/PDA.java @@ -35,11 +35,11 @@ public class PDA { case LiteralData: return LiteralMessage; - case Signatures: + case Signature: automaton.pushStack(msg); return OpenPgpMessage; - case OnePassSignatures: + case OnePassSignature: automaton.pushStack(ops); automaton.pushStack(msg); return OpenPgpMessage; @@ -63,7 +63,7 @@ public class PDA { StackAlphabet stackItem = automaton.popStack(); switch (input) { - case Signatures: + case Signature: if (stackItem == ops) { return CorrespondingSignature; } else { @@ -78,7 +78,7 @@ public class PDA { } case LiteralData: - case OnePassSignatures: + case OnePassSignature: case CompressedData: case EncryptedData: default: @@ -92,7 +92,7 @@ public class PDA { State transition(InputAlphabet input, PDA automaton) throws MalformedOpenPgpMessageException { StackAlphabet stackItem = automaton.popStack(); switch (input) { - case Signatures: + case Signature: if (stackItem == ops) { return CorrespondingSignature; } else { @@ -107,7 +107,7 @@ public class PDA { } case LiteralData: - case OnePassSignatures: + case OnePassSignature: case CompressedData: case EncryptedData: default: @@ -121,7 +121,7 @@ public class PDA { State transition(InputAlphabet input, PDA automaton) throws MalformedOpenPgpMessageException { StackAlphabet stackItem = automaton.popStack(); switch (input) { - case Signatures: + case Signature: if (stackItem == ops) { return CorrespondingSignature; } else { @@ -136,7 +136,7 @@ public class PDA { } case LiteralData: - case OnePassSignatures: + case OnePassSignature: case CompressedData: case EncryptedData: default: @@ -156,7 +156,7 @@ public class PDA { // premature end of stream throw new MalformedOpenPgpMessageException(this, input, stackItem); } - } else if (input == InputAlphabet.Signatures) { + } else if (input == InputAlphabet.Signature) { if (stackItem == ops) { return CorrespondingSignature; } diff --git a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStreamTest.java b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStreamTest.java index 46c521ef..1955eebe 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStreamTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStreamTest.java @@ -49,7 +49,6 @@ import org.pgpainless.util.Tuple; public class OpenPgpMessageInputStreamTest { - public static final String KEY = "-----BEGIN PGP PRIVATE KEY BLOCK-----\n" + "Version: PGPainless\n" + "Comment: DA05 848F 37D4 68E6 F982 C889 7A70 1FC6 904D 3F4C\n" + @@ -241,7 +240,7 @@ public class OpenPgpMessageInputStreamTest { System.out); } - public static void genSIG_LIT() throws PGPException, IOException { + public static void genSIG_COMP_LIT() throws PGPException, IOException { PGPSecretKeyRing secretKeys = PGPainless.readKeyRing().secretKeyRing(KEY); ByteArrayOutputStream msgOut = new ByteArrayOutputStream(); EncryptionStream signer = PGPainless.encryptAndOrSign() diff --git a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/automaton/PDATest.java b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/automaton/PDATest.java index 6e8a38d6..6dbb2cd6 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/automaton/PDATest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/automaton/PDATest.java @@ -30,9 +30,9 @@ public class PDATest { @Test public void testSimpleOpsSignedMesssageIsValid() throws MalformedOpenPgpMessageException { PDA automaton = new PDA(); - automaton.next(InputAlphabet.OnePassSignatures); + automaton.next(InputAlphabet.OnePassSignature); automaton.next(InputAlphabet.LiteralData); - automaton.next(InputAlphabet.Signatures); + automaton.next(InputAlphabet.Signature); automaton.next(InputAlphabet.EndOfSequence); assertTrue(automaton.isValid()); @@ -47,7 +47,7 @@ public class PDATest { @Test public void testSimplePrependSignedMessageIsValid() throws MalformedOpenPgpMessageException { PDA automaton = new PDA(); - automaton.next(InputAlphabet.Signatures); + automaton.next(InputAlphabet.Signature); automaton.next(InputAlphabet.LiteralData); automaton.next(InputAlphabet.EndOfSequence); @@ -63,10 +63,10 @@ public class PDATest { @Test public void testOPSSignedCompressedMessageIsValid() throws MalformedOpenPgpMessageException { PDA automaton = new PDA(); - automaton.next(InputAlphabet.OnePassSignatures); + automaton.next(InputAlphabet.OnePassSignature); automaton.next(InputAlphabet.CompressedData); // Here would be a nested PDA for the LiteralData packet - automaton.next(InputAlphabet.Signatures); + automaton.next(InputAlphabet.Signature); automaton.next(InputAlphabet.EndOfSequence); assertTrue(automaton.isValid()); From 43c369f1f9314c16190796e0fbf8277512248021 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Sun, 16 Oct 2022 15:47:47 +0200 Subject: [PATCH 039/763] It was the buffering. --- .../DecryptionBuilderInterface.java | 2 +- .../DecryptionStream.java | 60 +------ .../DecryptionStreamFactory.java | 33 +++- .../DecryptionStreamImpl.java | 65 +++++++ .../MessageMetadata.java | 22 +++ .../OpenPgpMessageInputStream.java | 166 ++++++++++++++---- ...vestigateMultiSEIPMessageHandlingTest.java | 17 +- ...ntDecryptionUsingNonEncryptionKeyTest.java | 4 +- 8 files changed, 256 insertions(+), 113 deletions(-) create mode 100644 pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionStreamImpl.java 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 07db42f0..b35911de 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 @@ -13,7 +13,7 @@ import org.bouncycastle.openpgp.PGPException; public interface DecryptionBuilderInterface { /** - * Create a {@link DecryptionStream} on an {@link InputStream} which contains the encrypted and/or signed data. + * Create a {@link DecryptionStreamImpl} on an {@link InputStream} which contains the encrypted and/or signed data. * * @param inputStream encrypted and/or signed data. * @return api handle diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionStream.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionStream.java index e3f1e720..c531f487 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionStream.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionStream.java @@ -1,65 +1,9 @@ -// SPDX-FileCopyrightText: 2018 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - package org.pgpainless.decryption_verification; -import java.io.IOException; -import java.io.InputStream; import javax.annotation.Nonnull; -import org.bouncycastle.util.io.Streams; - -/** - * Decryption Stream that handles updating and verification of detached signatures, - * as well as verification of integrity-protected input streams once the stream gets closed. - */ -public class DecryptionStream extends CloseForResultInputStream { - - private final InputStream inputStream; - private final IntegrityProtectedInputStream integrityProtectedInputStream; - private final InputStream armorStream; - - /** - * Create an input stream that handles decryption and - if necessary - integrity protection verification. - * - * @param wrapped underlying input stream - * @param resultBuilder builder for decryption metadata like algorithms, recipients etc. - * @param integrityProtectedInputStream in case of data encrypted using SEIP packet close this stream to check integrity - * @param armorStream armor stream to verify CRC checksums - */ - DecryptionStream(@Nonnull InputStream wrapped, - @Nonnull OpenPgpMetadata.Builder resultBuilder, - IntegrityProtectedInputStream integrityProtectedInputStream, - InputStream armorStream) { +public abstract class DecryptionStream extends CloseForResultInputStream { + public DecryptionStream(@Nonnull OpenPgpMetadata.Builder resultBuilder) { super(resultBuilder); - this.inputStream = wrapped; - this.integrityProtectedInputStream = integrityProtectedInputStream; - this.armorStream = armorStream; } - - @Override - public void close() throws IOException { - if (armorStream != null) { - Streams.drain(armorStream); - } - inputStream.close(); - if (integrityProtectedInputStream != null) { - integrityProtectedInputStream.close(); - } - super.close(); - } - - @Override - public int read() throws IOException { - int r = inputStream.read(); - return r; - } - - @Override - public int read(@Nonnull byte[] bytes, int offset, int length) throws IOException { - int read = inputStream.read(bytes, offset, length); - return read; - } - } 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 07be7c10..680f33e4 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 @@ -40,6 +40,7 @@ import org.bouncycastle.openpgp.operator.PBEDataDecryptorFactory; import org.bouncycastle.openpgp.operator.PGPContentVerifierBuilderProvider; import org.bouncycastle.openpgp.operator.PublicKeyDataDecryptorFactory; import org.bouncycastle.openpgp.operator.SessionKeyDataDecryptorFactory; +import org.graalvm.compiler.lir.amd64.AMD64BinaryConsumer; import org.pgpainless.PGPainless; import org.pgpainless.algorithm.CompressionAlgorithm; import org.pgpainless.algorithm.EncryptionPurpose; @@ -88,8 +89,28 @@ public final class DecryptionStreamFactory { public static DecryptionStream create(@Nonnull InputStream inputStream, - @Nonnull ConsumerOptions options) + @Nonnull ConsumerOptions options) throws PGPException, IOException { + OpenPgpInputStream openPgpInputStream = new OpenPgpInputStream(inputStream); + openPgpInputStream.reset(); + if (openPgpInputStream.isBinaryOpenPgp()) { + return new OpenPgpMessageInputStream(openPgpInputStream, options); + } else if (openPgpInputStream.isAsciiArmored()) { + ArmoredInputStream armorIn = ArmoredInputStreamFactory.get(openPgpInputStream); + if (armorIn.isClearText()) { + return createOld(openPgpInputStream, options); + } else { + return new OpenPgpMessageInputStream(armorIn, options); + } + } else if (openPgpInputStream.isNonOpenPgp()) { + return createOld(openPgpInputStream, options); + } else { + throw new IOException("What?"); + } + } + + public static DecryptionStream createOld(@Nonnull InputStream inputStream, + @Nonnull ConsumerOptions options) throws IOException, PGPException { DecryptionStreamFactory factory = new DecryptionStreamFactory(options); OpenPgpInputStream openPgpIn = new OpenPgpInputStream(inputStream); return factory.parseOpenPGPDataAndCreateDecryptionStream(openPgpIn); @@ -136,7 +157,7 @@ public final class DecryptionStreamFactory { if (openPgpIn.isNonOpenPgp() || options.isForceNonOpenPgpData()) { outerDecodingStream = openPgpIn; pgpInStream = wrapInVerifySignatureStream(outerDecodingStream, null); - return new DecryptionStream(pgpInStream, resultBuilder, integrityProtectedEncryptedInputStream, null); + return new DecryptionStreamImpl(pgpInStream, resultBuilder, integrityProtectedEncryptedInputStream, null); } // Data appears to be OpenPGP message, @@ -147,7 +168,7 @@ public final class DecryptionStreamFactory { objectFactory = ImplementationFactory.getInstance().getPGPObjectFactory(outerDecodingStream); // Parse OpenPGP message pgpInStream = processPGPPackets(objectFactory, 1); - return new DecryptionStream(pgpInStream, + return new DecryptionStreamImpl(pgpInStream, resultBuilder, integrityProtectedEncryptedInputStream, null); } @@ -161,7 +182,7 @@ public final class DecryptionStreamFactory { objectFactory = ImplementationFactory.getInstance().getPGPObjectFactory(outerDecodingStream); // Parse OpenPGP message pgpInStream = processPGPPackets(objectFactory, 1); - return new DecryptionStream(pgpInStream, + return new DecryptionStreamImpl(pgpInStream, resultBuilder, integrityProtectedEncryptedInputStream, outerDecodingStream); } @@ -170,7 +191,7 @@ public final class DecryptionStreamFactory { throw new PGPException("Not sure how to handle the input stream."); } - private DecryptionStream parseCleartextSignedMessage(ArmoredInputStream armorIn) + private DecryptionStreamImpl parseCleartextSignedMessage(ArmoredInputStream armorIn) throws IOException, PGPException { resultBuilder.setCompressionAlgorithm(CompressionAlgorithm.UNCOMPRESSED) .setFileEncoding(StreamEncoding.TEXT); @@ -185,7 +206,7 @@ public final class DecryptionStreamFactory { initializeDetachedSignatures(options.getDetachedSignatures()); InputStream verifyIn = wrapInVerifySignatureStream(multiPassStrategy.getMessageInputStream(), null); - return new DecryptionStream(verifyIn, resultBuilder, integrityProtectedEncryptedInputStream, + return new DecryptionStreamImpl(verifyIn, resultBuilder, integrityProtectedEncryptedInputStream, null); } diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionStreamImpl.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionStreamImpl.java new file mode 100644 index 00000000..27ace697 --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionStreamImpl.java @@ -0,0 +1,65 @@ +// SPDX-FileCopyrightText: 2018 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.decryption_verification; + +import java.io.IOException; +import java.io.InputStream; +import javax.annotation.Nonnull; + +import org.bouncycastle.util.io.Streams; + +/** + * Decryption Stream that handles updating and verification of detached signatures, + * as well as verification of integrity-protected input streams once the stream gets closed. + */ +public class DecryptionStreamImpl extends DecryptionStream { + + private final InputStream inputStream; + private final IntegrityProtectedInputStream integrityProtectedInputStream; + private final InputStream armorStream; + + /** + * Create an input stream that handles decryption and - if necessary - integrity protection verification. + * + * @param wrapped underlying input stream + * @param resultBuilder builder for decryption metadata like algorithms, recipients etc. + * @param integrityProtectedInputStream in case of data encrypted using SEIP packet close this stream to check integrity + * @param armorStream armor stream to verify CRC checksums + */ + DecryptionStreamImpl(@Nonnull InputStream wrapped, + @Nonnull OpenPgpMetadata.Builder resultBuilder, + IntegrityProtectedInputStream integrityProtectedInputStream, + InputStream armorStream) { + super(resultBuilder); + this.inputStream = wrapped; + this.integrityProtectedInputStream = integrityProtectedInputStream; + this.armorStream = armorStream; + } + + @Override + public void close() throws IOException { + if (armorStream != null) { + Streams.drain(armorStream); + } + inputStream.close(); + if (integrityProtectedInputStream != null) { + integrityProtectedInputStream.close(); + } + super.close(); + } + + @Override + public int read() throws IOException { + int r = inputStream.read(); + return r; + } + + @Override + public int read(@Nonnull byte[] bytes, int offset, int length) throws IOException { + int read = inputStream.read(bytes, offset, length); + return read; + } + +} diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/MessageMetadata.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/MessageMetadata.java index cbf251a8..87f6d7f6 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/MessageMetadata.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/MessageMetadata.java @@ -69,6 +69,28 @@ public class MessageMetadata { }; } + public @Nullable SessionKey getSessionKey() { + Iterator sessionKeys = getSessionKeys(); + if (sessionKeys.hasNext()) { + return sessionKeys.next(); + } + return null; + } + + public @Nonnull Iterator getSessionKeys() { + return new LayerIterator(message) { + @Override + boolean matches(Nested layer) { + return layer instanceof EncryptedData; + } + + @Override + SessionKey getProperty(Layer last) { + return ((EncryptedData) last).getSessionKey(); + } + }; + } + public String getFilename() { return findLiteralData().getFileName(); } diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java index 75359de1..7e34d329 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java @@ -4,6 +4,7 @@ package org.pgpainless.decryption_verification; +import java.io.BufferedInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; @@ -12,6 +13,7 @@ import java.util.ArrayList; import java.util.Collection; import java.util.List; import java.util.Stack; +import javax.annotation.Nonnull; import org.bouncycastle.bcpg.BCPGInputStream; import org.bouncycastle.openpgp.PGPCompressedData; @@ -28,7 +30,6 @@ import org.bouncycastle.openpgp.PGPPublicKeyRing; import org.bouncycastle.openpgp.PGPSecretKey; import org.bouncycastle.openpgp.PGPSecretKeyRing; import org.bouncycastle.openpgp.PGPSignature; -import org.bouncycastle.openpgp.PGPUtil; import org.bouncycastle.openpgp.operator.PBEDataDecryptorFactory; import org.bouncycastle.openpgp.operator.PGPContentVerifierBuilderProvider; import org.bouncycastle.openpgp.operator.PublicKeyDataDecryptorFactory; @@ -46,25 +47,27 @@ import org.pgpainless.decryption_verification.automaton.StackAlphabet; import org.pgpainless.exception.MalformedOpenPgpMessageException; import org.pgpainless.exception.MessageNotIntegrityProtectedException; import org.pgpainless.exception.MissingDecryptionMethodException; +import org.pgpainless.exception.SignatureValidationException; +import org.pgpainless.exception.UnacceptableAlgorithmException; import org.pgpainless.implementation.ImplementationFactory; +import org.pgpainless.key.SubkeyIdentifier; import org.pgpainless.key.info.KeyRingInfo; import org.pgpainless.key.protection.SecretKeyRingProtector; import org.pgpainless.key.protection.UnlockSecretKey; +import org.pgpainless.policy.Policy; import org.pgpainless.signature.SignatureUtils; import org.pgpainless.util.Passphrase; +import org.pgpainless.util.SessionKey; import org.pgpainless.util.Tuple; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import javax.annotation.Nonnull; - -public class OpenPgpMessageInputStream extends InputStream { +public class OpenPgpMessageInputStream extends DecryptionStream { private static final Logger LOGGER = LoggerFactory.getLogger(OpenPgpMessageInputStream.class); // Options to consume the data protected final ConsumerOptions options; - protected final OpenPgpMetadata.Builder resultBuilder; // Pushdown Automaton to verify validity of OpenPGP packet sequence in an OpenPGP message protected final PDA automaton = new PDA(); // InputStream of OpenPGP packets @@ -76,6 +79,7 @@ public class OpenPgpMessageInputStream extends InputStream { private final Signatures signatures; private final MessageMetadata.Layer metadata; + private final Policy policy; public OpenPgpMessageInputStream(InputStream inputStream, ConsumerOptions options) throws IOException, PGPException { @@ -84,10 +88,11 @@ public class OpenPgpMessageInputStream extends InputStream { OpenPgpMessageInputStream(InputStream inputStream, ConsumerOptions options, MessageMetadata.Layer metadata) throws PGPException, IOException { + super(OpenPgpMetadata.getBuilder()); + this.policy = PGPainless.getPolicy(); this.options = options; this.metadata = metadata; - this.resultBuilder = OpenPgpMetadata.getBuilder(); this.signatures = new Signatures(options); // Add detached signatures only on the outermost OpenPgpMessageInputStream @@ -210,7 +215,8 @@ public class OpenPgpMessageInputStream extends InputStream { PGPCompressedData compressedData = packetInputStream.readCompressedData(); MessageMetadata.CompressedData compressionLayer = new MessageMetadata.CompressedData( CompressionAlgorithm.fromId(compressedData.getAlgorithm())); - nestedInputStream = new OpenPgpMessageInputStream(compressedData.getDataStream(), options, compressionLayer); + InputStream decompressed = compressedData.getDataStream(); + nestedInputStream = new OpenPgpMessageInputStream(buffer(decompressed), options, compressionLayer); } private void processLiteralData() throws IOException { @@ -232,19 +238,25 @@ public class OpenPgpMessageInputStream extends InputStream { // Try session key if (options.getSessionKey() != null) { + SessionKey sessionKey = options.getSessionKey(); + if (!policy.getSymmetricKeyDecryptionAlgorithmPolicy().isAcceptable(sessionKey.getAlgorithm())) { + throw new UnacceptableAlgorithmException("Symmetric algorithm " + sessionKey.getAlgorithm() + " is not acceptable."); + } SessionKeyDataDecryptorFactory decryptorFactory = ImplementationFactory.getInstance() - .getSessionKeyDataDecryptorFactory(options.getSessionKey()); + .getSessionKeyDataDecryptorFactory(sessionKey); // TODO: Replace with encDataList.addSessionKeyDecryptionMethod(sessionKey) PGPEncryptedData esk = esks.all().get(0); try { - MessageMetadata.EncryptedData encryptedData = new MessageMetadata.EncryptedData(options.getSessionKey().getAlgorithm()); + MessageMetadata.EncryptedData encryptedData = new MessageMetadata.EncryptedData(sessionKey.getAlgorithm()); if (esk instanceof PGPPBEEncryptedData) { PGPPBEEncryptedData skesk = (PGPPBEEncryptedData) esk; - nestedInputStream = new OpenPgpMessageInputStream(skesk.getDataStream(decryptorFactory), options, encryptedData); + InputStream decrypted = skesk.getDataStream(decryptorFactory); + nestedInputStream = new OpenPgpMessageInputStream(buffer(decrypted), options, encryptedData); return true; } else if (esk instanceof PGPPublicKeyEncryptedData) { PGPPublicKeyEncryptedData pkesk = (PGPPublicKeyEncryptedData) esk; - nestedInputStream = new OpenPgpMessageInputStream(pkesk.getDataStream(decryptorFactory), options, encryptedData); + InputStream decrypted = pkesk.getDataStream(decryptorFactory); + nestedInputStream = new OpenPgpMessageInputStream(buffer(decrypted), options, encryptedData); return true; } else { throw new RuntimeException("Unknown ESK class type: " + esk.getClass().getName()); @@ -263,7 +275,7 @@ public class OpenPgpMessageInputStream extends InputStream { InputStream decrypted = skesk.getDataStream(decryptorFactory); MessageMetadata.EncryptedData encryptedData = new MessageMetadata.EncryptedData( SymmetricKeyAlgorithm.requireFromId(skesk.getSymmetricAlgorithm(decryptorFactory))); - nestedInputStream = new OpenPgpMessageInputStream(decrypted, options, encryptedData); + nestedInputStream = new OpenPgpMessageInputStream(buffer(decrypted), options, encryptedData); return true; } catch (PGPException e) { // password mismatch? Try next password @@ -286,14 +298,20 @@ public class OpenPgpMessageInputStream extends InputStream { PublicKeyDataDecryptorFactory decryptorFactory = ImplementationFactory.getInstance() .getPublicKeyDataDecryptorFactory(privateKey); try { + SymmetricKeyAlgorithm symAlg = SymmetricKeyAlgorithm.requireFromId(pkesk.getSymmetricAlgorithm(decryptorFactory)); + if (!policy.getSymmetricKeyDecryptionAlgorithmPolicy().isAcceptable(symAlg)) { + throw new UnacceptableAlgorithmException("Symmetric-key algorithm " + symAlg + " is not acceptable."); + } InputStream decrypted = pkesk.getDataStream(decryptorFactory); MessageMetadata.EncryptedData encryptedData = new MessageMetadata.EncryptedData( SymmetricKeyAlgorithm.requireFromId(pkesk.getSymmetricAlgorithm(decryptorFactory))); - nestedInputStream = new OpenPgpMessageInputStream(PGPUtil.getDecoderStream(decrypted), options, encryptedData); + nestedInputStream = new OpenPgpMessageInputStream(buffer(decrypted), options, encryptedData); return true; } catch (PGPException e) { - // hm :/ + if (e instanceof UnacceptableAlgorithmException) { + throw e; + } } } @@ -309,7 +327,7 @@ public class OpenPgpMessageInputStream extends InputStream { InputStream decrypted = pkesk.getDataStream(decryptorFactory); MessageMetadata.EncryptedData encryptedData = new MessageMetadata.EncryptedData( SymmetricKeyAlgorithm.requireFromId(pkesk.getSymmetricAlgorithm(decryptorFactory))); - nestedInputStream = new OpenPgpMessageInputStream(decrypted, options, encryptedData); + nestedInputStream = new OpenPgpMessageInputStream(buffer(decrypted), options, encryptedData); return true; } catch (PGPException e) { // hm :/ @@ -321,6 +339,10 @@ public class OpenPgpMessageInputStream extends InputStream { return false; } + private static InputStream buffer(InputStream inputStream) { + return new BufferedInputStream(inputStream); + } + private List> findPotentialDecryptionKeys(PGPPublicKeyEncryptedData pkesk) { int algorithm = pkesk.getAlgorithm(); List> decryptionKeyCandidates = new ArrayList<>(); @@ -420,6 +442,7 @@ public class OpenPgpMessageInputStream extends InputStream { @Override public void close() throws IOException { + super.close(); if (closed) { automaton.assertValid(); return; @@ -502,6 +525,8 @@ public class OpenPgpMessageInputStream extends InputStream { final List correspondingSignatures; boolean isLiteral = true; + final List verified = new ArrayList<>(); + private Signatures(ConsumerOptions options) { this.options = options; this.detachedSignatures = new ArrayList<>(); @@ -521,19 +546,19 @@ public class OpenPgpMessageInputStream extends InputStream { long keyId = SignatureUtils.determineIssuerKeyId(signature); PGPPublicKeyRing certificate = findCertificate(keyId); initialize(signature, certificate, keyId); - this.detachedSignatures.add(new SIG(signature)); + this.detachedSignatures.add(new SIG(signature, certificate, keyId)); } void addPrependedSignature(PGPSignature signature) { long keyId = SignatureUtils.determineIssuerKeyId(signature); PGPPublicKeyRing certificate = findCertificate(keyId); initialize(signature, certificate, keyId); - this.prependedSignatures.add(new SIG(signature)); + this.prependedSignatures.add(new SIG(signature, certificate, keyId)); } void addOnePassSignature(PGPOnePassSignature signature) { PGPPublicKeyRing certificate = findCertificate(signature.getKeyID()); - OPS ops = new OPS(signature); + OPS ops = new OPS(signature, certificate, signature.getKeyID()); ops.init(certificate); onePassSignatures.add(ops); @@ -546,7 +571,7 @@ public class OpenPgpMessageInputStream extends InputStream { void addCorrespondingOnePassSignature(PGPSignature signature) { for (int i = onePassSignatures.size() - 1; i >= 0; i--) { OPS onePassSignature = onePassSignatures.get(i); - if (onePassSignature.signature.getKeyID() != signature.getKeyID()) { + if (onePassSignature.opSignature.getKeyID() != signature.getKeyID()) { continue; } if (onePassSignature.finished) { @@ -554,8 +579,8 @@ public class OpenPgpMessageInputStream extends InputStream { } boolean verified = onePassSignature.verify(signature); - log("One-Pass-Signature by " + Long.toHexString(onePassSignature.signature.getKeyID()) + " is " + (verified ? "verified" : "unverified")); - System.out.println(onePassSignature); + log("One-Pass-Signature by " + Long.toHexString(onePassSignature.opSignature.getKeyID()) + " is " + (verified ? "verified" : "unverified")); + log(onePassSignature.toString()); break; } } @@ -666,14 +691,20 @@ public class OpenPgpMessageInputStream extends InputStream { public void finish() { for (SIG detached : detachedSignatures) { boolean verified = detached.verify(); + if (verified) { + this.verified.add(detached.signature); + } log("Detached Signature by " + Long.toHexString(detached.signature.getKeyID()) + " is " + (verified ? "verified" : "unverified")); - System.out.println(detached); + log(detached.toString()); } for (SIG prepended : prependedSignatures) { boolean verified = prepended.verify(); + if (verified) { + this.verified.add(prepended.signature); + } log("Prepended Signature by " + Long.toHexString(prepended.signature.getKeyID()) + " is " + (verified ? "verified" : "unverified")); - System.out.println(prepended); + log(prepended.toString()); } } @@ -701,11 +732,15 @@ public class OpenPgpMessageInputStream extends InputStream { static class SIG { ByteArrayOutputStream bytes = new ByteArrayOutputStream(); PGPSignature signature; + PGPPublicKeyRing certificate; + long keyId; boolean finished; boolean valid; - public SIG(PGPSignature signature) { + public SIG(PGPSignature signature, PGPPublicKeyRing certificate, long keyId) { this.signature = signature; + this.certificate = certificate; + this.keyId = keyId; } public void init(PGPPublicKeyRing certificate) { @@ -713,6 +748,9 @@ public class OpenPgpMessageInputStream extends InputStream { } public boolean verify() { + if (finished) { + throw new IllegalStateException("Already finished."); + } finished = true; try { valid = this.signature.verify(); @@ -780,26 +818,32 @@ public class OpenPgpMessageInputStream extends InputStream { static class OPS { ByteArrayOutputStream bytes = new ByteArrayOutputStream(); - PGPOnePassSignature signature; + PGPOnePassSignature opSignature; + PGPSignature signature; + PGPPublicKeyRing certificate; + long keyId; boolean finished; boolean valid; - public OPS(PGPOnePassSignature signature) { - this.signature = signature; + public OPS(PGPOnePassSignature signature, PGPPublicKeyRing certificate, long keyId) { + this.opSignature = signature; + this.certificate = certificate; + this.keyId = keyId; } public void init(PGPPublicKeyRing certificate) { - initialize(signature, certificate); + initialize(opSignature, certificate); } public boolean verify(PGPSignature signature) { - if (this.signature.getKeyID() != signature.getKeyID()) { + if (this.opSignature.getKeyID() != signature.getKeyID()) { // nope return false; } + this.signature = signature; finished = true; try { - valid = this.signature.verify(signature); + valid = this.opSignature.verify(signature); } catch (PGPException e) { log("Cannot verify OPS " + signature.getKeyID()); } @@ -811,7 +855,7 @@ public class OpenPgpMessageInputStream extends InputStream { log("Updating finished sig!"); return; } - signature.update(b); + opSignature.update(b); bytes.write(b); } @@ -820,7 +864,7 @@ public class OpenPgpMessageInputStream extends InputStream { log("Updating finished sig!"); return; } - signature.update(bytes, off, len); + opSignature.update(bytes, off, len); this.bytes.write(bytes, off, len); } @@ -833,7 +877,7 @@ public class OpenPgpMessageInputStream extends InputStream { String SIG1f = "c2c13b0400010a006f058262c806350910fbfcc82a015e7330471400000000001e002073616c74406e6f746174696f6e732e736571756f69612d7067702e6f7267b0409ed8ea96dac66447bdff5b7b60c9f80a0ab91d257029153dc3b6d8c27b98162104d1a66e1a23b182c9980f788cfbfcc82a015e7330000029640c00846b5096d92474fd446cc7edaf9f14572cab93a80e12384c1e829f95debc6e8373c2ce5402be53dc1a18cf92a0ed909e0fb38855713ef8ffb13502ffac7c830fa254cc1aa6c666a97b0cc3bc176538f6913d3b8e8981a65cc42df10e0f39e4d0a06dfe961437b59a71892f4fca1116aed15123ea0d86a7b2ce47dd9d3ef22d920631bc011e82babe03ad5d72b3ba7f95bf646f20ccf6f7a4d95de37397c76c7d53741458e51ab6074007f61181c7b88b7c98f5b7510c8dfa3be01f4841501679478b15c5249d928e2a10d15ec63efa1500b994d5bfb32ffb174a976116930eb97a111e6dfd4c5e43e04a5d76ba74806a62fda63a8c3f53f6eebaf852892340e81dd08bbf348454a2cf525aeb512cf33aeeee78465ee4c442e41cc45ac4e3bb0c3333677aa60332ee7f464d9020f8554b82d619872477cca18d8431888f4ae8abe5894e9720f759c410cd7991db12703dc147040dd0d3758223e0b75de6ceae49c1a0c2c45efedeb7114ae785cc886afdc45c82172e4476e1ab5b86dc4314dd76"; String SIG2 = "c2c10400010a006f058262c806350910fbfcc82a015e7330471400000000001e002073616c74406e6f746174696f6e732e736571756f69612d7067702e6f7267a4d9c117dc7ba3a7e9270856f128d2ab271743eac3cb5750b22a89bd5fd60753162104d1a66e1a23b182c9980f788cfbfcc82a015e73300000b8400bff796c20fa8b25ff7a42686338e06417a2966e85a0fc2723c928bef6cd19d34cf5e7d55ada33080613012dadb79e0278e59d9e7ed7d2d6102912a5f768c2e75b60099225c3d8bfe0c123240188b80dbee89b9b3bd5b13ccc662abc37e2129b6968adac9aba43aa778c0fe4fe337591ee87a96a29a013debc83555293c877144fc676aa1b03782c501949521a320adf6ad96c4f2e036b52a18369c637fdc49033696a84d03a69580b953187fce5aca6fb26fc8815da9f3b513bfe8e304f33ecb4b521aeb7d09c4a284ea66123bd0d6a358b2526d762ca110e1f7f20b3038d774b64d5cfd34e2213765828359d7afc5bf24d5270e99d80c3c1568fa01624b6ea1e9ce4e6890ce9bacf6611a45d41e2671f68f5b096446bf08d27ce75608425b2e3ab92146229ad1fcd8224aca5b5f73960506e7df07bfbf3664348e8ecbfb2eb467b9cfe412cb377a6ee2eb5fd11be9cf9208fe9a74c296f52cfa02a1eb0519ad9a8349bf6ccd6495feb7e391451bf96e08a0798883dee5974e47cbf3b51f111b6d3"; String SIG2f = "c2c13b0400010a006f058262c806350910fbfcc82a015e7330471400000000001e002073616c74406e6f746174696f6e732e736571756f69612d7067702e6f7267a4d9c117dc7ba3a7e9270856f128d2ab271743eac3cb5750b22a89bd5fd60753162104d1a66e1a23b182c9980f788cfbfcc82a015e73300000b8400bff796c20fa8b25ff7a42686338e06417a2966e85a0fc2723c928bef6cd19d34cf5e7d55ada33080613012dadb79e0278e59d9e7ed7d2d6102912a5f768c2e75b60099225c3d8bfe0c123240188b80dbee89b9b3bd5b13ccc662abc37e2129b6968adac9aba43aa778c0fe4fe337591ee87a96a29a013debc83555293c877144fc676aa1b03782c501949521a320adf6ad96c4f2e036b52a18369c637fdc49033696a84d03a69580b953187fce5aca6fb26fc8815da9f3b513bfe8e304f33ecb4b521aeb7d09c4a284ea66123bd0d6a358b2526d762ca110e1f7f20b3038d774b64d5cfd34e2213765828359d7afc5bf24d5270e99d80c3c1568fa01624b6ea1e9ce4e6890ce9bacf6611a45d41e2671f68f5b096446bf08d27ce75608425b2e3ab92146229ad1fcd8224aca5b5f73960506e7df07bfbf3664348e8ecbfb2eb467b9cfe412cb377a6ee2eb5fd11be9cf9208fe9a74c296f52cfa02a1eb0519ad9a8349bf6ccd6495feb7e391451bf96e08a0798883dee5974e47cbf3b51f111b6d3"; - String out = "last=" + signature.isContaining() + "\n"; + String out = "last=" + opSignature.isContaining() + "\n"; String hex = Hex.toHexString(bytes.toByteArray()); while (hex.contains(OPS)) { @@ -863,10 +907,64 @@ public class OpenPgpMessageInputStream extends InputStream { } } + @Override + public OpenPgpMetadata getResult() { + MessageMetadata m = getMetadata(); + resultBuilder.setCompressionAlgorithm(m.getCompressionAlgorithm()); + resultBuilder.setModificationDate(m.getModificationDate()); + resultBuilder.setFileName(m.getFilename()); + resultBuilder.setFileEncoding(m.getFormat()); + resultBuilder.setSessionKey(m.getSessionKey()); + + for (Signatures.OPS ops : signatures.onePassSignatures) { + if (!ops.finished) { + continue; + } + + SubkeyIdentifier identifier = new SubkeyIdentifier(ops.certificate, ops.keyId); + SignatureVerification verification = new SignatureVerification(ops.signature, identifier); + if (ops.valid) { + resultBuilder.addVerifiedInbandSignature(verification); + } else { + resultBuilder.addInvalidInbandSignature(verification, new SignatureValidationException("Incorrect signature.")); + } + } + + for (Signatures.SIG prep : signatures.prependedSignatures) { + if (!prep.finished) { + continue; + } + + SubkeyIdentifier identifier = new SubkeyIdentifier(prep.certificate, prep.keyId); + SignatureVerification verification = new SignatureVerification(prep.signature, identifier); + if (prep.valid) { + resultBuilder.addVerifiedInbandSignature(verification); + } else { + resultBuilder.addInvalidInbandSignature(verification, new SignatureValidationException("Incorrect signature.")); + } + } + + for (Signatures.SIG det : signatures.detachedSignatures) { + if (!det.finished) { + continue; + } + + SubkeyIdentifier identifier = new SubkeyIdentifier(det.certificate, det.keyId); + SignatureVerification verification = new SignatureVerification(det.signature, identifier); + if (det.valid) { + resultBuilder.addVerifiedDetachedSignature(verification); + } else { + resultBuilder.addInvalidDetachedSignature(verification, new SignatureValidationException("Incorrect signature.")); + } + } + + return resultBuilder.build(); + } + static void log(String message) { LOGGER.debug(message); // CHECKSTYLE:OFF - System.out.println(message); + // System.out.println(message); // CHECKSTYLE:ON } diff --git a/pgpainless-core/src/test/java/investigations/InvestigateMultiSEIPMessageHandlingTest.java b/pgpainless-core/src/test/java/investigations/InvestigateMultiSEIPMessageHandlingTest.java index 3350203a..d31a0e0f 100644 --- a/pgpainless-core/src/test/java/investigations/InvestigateMultiSEIPMessageHandlingTest.java +++ b/pgpainless-core/src/test/java/investigations/InvestigateMultiSEIPMessageHandlingTest.java @@ -6,6 +6,7 @@ package investigations; import static org.junit.jupiter.api.Assertions.assertArrayEquals; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; @@ -32,9 +33,8 @@ import org.pgpainless.algorithm.SignatureType; import org.pgpainless.algorithm.SymmetricKeyAlgorithm; import org.pgpainless.decryption_verification.ConsumerOptions; import org.pgpainless.decryption_verification.DecryptionStream; -import org.pgpainless.decryption_verification.OpenPgpMetadata; +import org.pgpainless.exception.MalformedOpenPgpMessageException; import org.pgpainless.implementation.ImplementationFactory; -import org.pgpainless.key.SubkeyIdentifier; import org.pgpainless.key.info.KeyRingInfo; import org.pgpainless.key.protection.UnlockSecretKey; import org.pgpainless.util.Passphrase; @@ -177,7 +177,7 @@ public class InvestigateMultiSEIPMessageHandlingTest { } @Test - public void testDecryptAndVerifyDoesIgnoreAppendedSEIPData() throws IOException, PGPException { + public void testDecryptAndVerifyDetectsAppendedSEIPData() throws IOException, PGPException { PGPSecretKeyRing ring1 = PGPainless.readKeyRing().secretKeyRing(KEY1); PGPSecretKeyRing ring2 = PGPainless.readKeyRing().secretKeyRing(KEY2); @@ -191,15 +191,6 @@ public class InvestigateMultiSEIPMessageHandlingTest { .withOptions(options); ByteArrayOutputStream out = new ByteArrayOutputStream(); - Streams.pipeAll(decryptionStream, out); - decryptionStream.close(); - - assertArrayEquals(data1.getBytes(StandardCharsets.UTF_8), out.toByteArray()); - OpenPgpMetadata metadata = decryptionStream.getResult(); - assertEquals(1, metadata.getVerifiedSignatures().size(), - "The first SEIP packet is signed exactly only by the signing key of ring1."); - assertEquals( - new SubkeyIdentifier(ring1, new KeyRingInfo(ring1).getSigningSubkeys().get(0).getKeyID()), - metadata.getVerifiedSignatures().keySet().iterator().next()); + assertThrows(MalformedOpenPgpMessageException.class, () -> Streams.pipeAll(decryptionStream, out)); } } diff --git a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/PreventDecryptionUsingNonEncryptionKeyTest.java b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/PreventDecryptionUsingNonEncryptionKeyTest.java index 851a1bef..7474301b 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/PreventDecryptionUsingNonEncryptionKeyTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/PreventDecryptionUsingNonEncryptionKeyTest.java @@ -8,6 +8,8 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.EOFException; import java.io.IOException; import java.nio.charset.StandardCharsets; @@ -185,7 +187,7 @@ public class PreventDecryptionUsingNonEncryptionKeyTest { decryptionStream.close(); OpenPgpMetadata metadata = decryptionStream.getResult(); - assertEquals(metadata.getDecryptionKey(), new SubkeyIdentifier(secretKeys, secretKeys.getPublicKey().getKeyID())); + assertEquals(new SubkeyIdentifier(secretKeys, secretKeys.getPublicKey().getKeyID()), metadata.getDecryptionKey()); } @Test From a9f77ea10019ca3889198d4dc3f1253aa3e81073 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Sun, 16 Oct 2022 18:15:31 +0200 Subject: [PATCH 040/763] Cleaning up and collect signature verifications --- .../MessageMetadata.java | 85 +++- .../OpenPgpMessageInputStream.java | 375 ++++++++---------- .../OpenPgpMessageInputStreamTest.java | 19 + 3 files changed, 262 insertions(+), 217 deletions(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/MessageMetadata.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/MessageMetadata.java index 87f6d7f6..bb2f5c76 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/MessageMetadata.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/MessageMetadata.java @@ -91,6 +91,62 @@ public class MessageMetadata { }; } + public @Nonnull List getVerifiedSignatures() { + List verifications = new ArrayList<>(); + Iterator> verificationsByLayer = getVerifiedSignaturesByLayer(); + while (verificationsByLayer.hasNext()) { + verifications.addAll(verificationsByLayer.next()); + } + return verifications; + } + + public @Nonnull Iterator> getVerifiedSignaturesByLayer() { + return new LayerIterator>(message) { + @Override + boolean matches(Nested layer) { + return layer instanceof Layer; + } + + @Override + boolean matches(Layer layer) { + return true; + } + + @Override + List getProperty(Layer last) { + return new ArrayList<>(last.getVerifiedSignatures()); + } + }; + } + + public @Nonnull List getRejectedSignatures() { + List rejected = new ArrayList<>(); + Iterator> rejectedByLayer = getRejectedSignaturesByLayer(); + while (rejectedByLayer.hasNext()) { + rejected.addAll(rejectedByLayer.next()); + } + return rejected; + } + + public @Nonnull Iterator> getRejectedSignaturesByLayer() { + return new LayerIterator>(message) { + @Override + boolean matches(Nested layer) { + return layer instanceof Layer; + } + + @Override + boolean matches(Layer layer) { + return true; + } + + @Override + List getProperty(Layer last) { + return new ArrayList<>(last.getFailedSignatures()); + } + }; + } + public String getFilename() { return findLiteralData().getFileName(); } @@ -132,6 +188,14 @@ public class MessageMetadata { public List getFailedSignatures() { return new ArrayList<>(failedSignatures); } + + void addVerifiedSignature(SignatureVerification signatureVerification) { + verifiedSignatures.add(signatureVerification); + } + + void addFailedSignature(SignatureVerification.Failure failure) { + failedSignatures.add(failure); + } } public interface Nested { @@ -223,9 +287,11 @@ public class MessageMetadata { private abstract static class LayerIterator implements Iterator { private Nested current; Layer last = null; + Message parent; LayerIterator(Message message) { super(); + this.parent = message; this.current = message.child; if (matches(current)) { last = (Layer) current; @@ -234,6 +300,9 @@ public class MessageMetadata { @Override public boolean hasNext() { + if (parent != null && matches(parent)) { + return true; + } if (last == null) { findNext(); } @@ -242,6 +311,11 @@ public class MessageMetadata { @Override public O next() { + if (parent != null && matches(parent)) { + O property = getProperty(parent); + parent = null; + return property; + } if (last == null) { findNext(); } @@ -263,7 +337,16 @@ public class MessageMetadata { } } - abstract boolean matches(Nested layer); + boolean matches(Nested layer) { + return false; + } + + boolean matches(Layer layer) { + if (layer instanceof Nested) { + return matches((Nested) layer); + } + return false; + } abstract O getProperty(Layer last); } diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java index 7e34d329..9e26dc20 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java @@ -34,7 +34,6 @@ import org.bouncycastle.openpgp.operator.PBEDataDecryptorFactory; import org.bouncycastle.openpgp.operator.PGPContentVerifierBuilderProvider; import org.bouncycastle.openpgp.operator.PublicKeyDataDecryptorFactory; import org.bouncycastle.openpgp.operator.SessionKeyDataDecryptorFactory; -import org.bouncycastle.util.encoders.Hex; import org.pgpainless.PGPainless; import org.pgpainless.algorithm.CompressionAlgorithm; import org.pgpainless.algorithm.EncryptionPurpose; @@ -81,16 +80,27 @@ public class OpenPgpMessageInputStream extends DecryptionStream { private final MessageMetadata.Layer metadata; private final Policy policy; - public OpenPgpMessageInputStream(InputStream inputStream, ConsumerOptions options) + public OpenPgpMessageInputStream(@Nonnull InputStream inputStream, + @Nonnull ConsumerOptions options) throws IOException, PGPException { - this(inputStream, options, new MessageMetadata.Message()); + this(inputStream, options, PGPainless.getPolicy()); } - OpenPgpMessageInputStream(InputStream inputStream, ConsumerOptions options, MessageMetadata.Layer metadata) + public OpenPgpMessageInputStream(@Nonnull InputStream inputStream, + @Nonnull ConsumerOptions options, + @Nonnull Policy policy) + throws PGPException, IOException { + this(inputStream, options, new MessageMetadata.Message(), policy); + } + + protected OpenPgpMessageInputStream(@Nonnull InputStream inputStream, + @Nonnull ConsumerOptions options, + @Nonnull MessageMetadata.Layer metadata, + @Nonnull Policy policy) throws PGPException, IOException { super(OpenPgpMetadata.getBuilder()); - this.policy = PGPainless.getPolicy(); + this.policy = policy; this.options = options; this.metadata = metadata; this.signatures = new Signatures(options); @@ -100,6 +110,7 @@ public class OpenPgpMessageInputStream extends DecryptionStream { this.signatures.addDetachedSignatures(options.getDetachedSignatures()); } + // tee out packet bytes for signature verification packetInputStream = new TeeBCPGInputStream(BCPGInputStream.wrap(inputStream), signatures); // *omnomnom* @@ -125,37 +136,30 @@ public class OpenPgpMessageInputStream extends DecryptionStream { private void consumePackets() throws IOException, PGPException { OpenPgpPacket nextPacket; - loop: while ((nextPacket = packetInputStream.nextPacketTag()) != null) { + + loop: // we break this when we go deeper. + while ((nextPacket = packetInputStream.nextPacketTag()) != null) { signatures.nextPacket(nextPacket); switch (nextPacket) { // Literal Data - the literal data content is the new input stream case LIT: - automaton.next(InputAlphabet.LiteralData); processLiteralData(); break loop; // Compressed Data - the content contains another OpenPGP message case COMP: - automaton.next(InputAlphabet.CompressedData); processCompressedData(); break loop; // One Pass Signature case OPS: - automaton.next(InputAlphabet.OnePassSignature); - PGPOnePassSignature onePassSignature = readOnePassSignature(); - signatures.addOnePassSignature(onePassSignature); + processOnePassSignature(); break; // Signature - either prepended to the message, or corresponding to a One Pass Signature case SIG: - // true if Signature corresponds to OnePassSignature - boolean isSigForOPS = automaton.peekStack() == StackAlphabet.ops; - automaton.next(InputAlphabet.Signature); - - processSignature(isSigForOPS); - + processSignature(); break; // Encrypted Data (ESKs and SED/SEIPD are parsed the same by BC) @@ -163,14 +167,13 @@ public class OpenPgpMessageInputStream extends DecryptionStream { case SKESK: case SED: case SEIPD: - automaton.next(InputAlphabet.EncryptedData); if (processEncryptedData()) { break loop; } throw new MissingDecryptionMethodException("No working decryption method found."); - // Marker Packets need to be skipped and ignored + // Marker Packets need to be skipped and ignored case MARKER: packetInputStream.readMarker(); break; @@ -200,36 +203,51 @@ public class OpenPgpMessageInputStream extends DecryptionStream { } } - private void processSignature(boolean isSigForOPS) throws PGPException, IOException { - PGPSignature signature = readSignature(); - if (isSigForOPS) { - signatures.leaveNesting(); // TODO: Only leave nesting if all OPSs are dealt with - signatures.addCorrespondingOnePassSignature(signature); - } else { - signatures.addPrependedSignature(signature); - } + private void processLiteralData() throws IOException { + automaton.next(InputAlphabet.LiteralData); + PGPLiteralData literalData = packetInputStream.readLiteralData(); + this.metadata.setChild(new MessageMetadata.LiteralData( + literalData.getFileName(), + literalData.getModificationTime(), + StreamEncoding.requireFromCode(literalData.getFormat()))); + nestedInputStream = literalData.getDataStream(); } private void processCompressedData() throws IOException, PGPException { + automaton.next(InputAlphabet.CompressedData); signatures.enterNesting(); PGPCompressedData compressedData = packetInputStream.readCompressedData(); MessageMetadata.CompressedData compressionLayer = new MessageMetadata.CompressedData( CompressionAlgorithm.fromId(compressedData.getAlgorithm())); InputStream decompressed = compressedData.getDataStream(); - nestedInputStream = new OpenPgpMessageInputStream(buffer(decompressed), options, compressionLayer); + nestedInputStream = new OpenPgpMessageInputStream(buffer(decompressed), options, compressionLayer, policy); } - private void processLiteralData() throws IOException { - PGPLiteralData literalData = packetInputStream.readLiteralData(); - this.metadata.setChild(new MessageMetadata.LiteralData(literalData.getFileName(), literalData.getModificationTime(), - StreamEncoding.requireFromCode(literalData.getFormat()))); - nestedInputStream = literalData.getDataStream(); + private void processOnePassSignature() throws PGPException, IOException { + automaton.next(InputAlphabet.OnePassSignature); + PGPOnePassSignature onePassSignature = packetInputStream.readOnePassSignature(); + signatures.addOnePassSignature(onePassSignature); + } + + private void processSignature() throws PGPException, IOException { + // true if Signature corresponds to OnePassSignature + boolean isSigForOPS = automaton.peekStack() == StackAlphabet.ops; + automaton.next(InputAlphabet.Signature); + PGPSignature signature = packetInputStream.readSignature(); + if (isSigForOPS) { + signatures.leaveNesting(); // TODO: Only leave nesting if all OPSs of the nesting layer are dealt with + signatures.addCorrespondingOnePassSignature(signature, metadata); + } else { + signatures.addPrependedSignature(signature); + } } private boolean processEncryptedData() throws IOException, PGPException { + automaton.next(InputAlphabet.EncryptedData); PGPEncryptedDataList encDataList = packetInputStream.readEncryptedDataList(); // TODO: Replace with !encDataList.isIntegrityProtected() + // once BC ships it if (!encDataList.get(0).isIntegrityProtected()) { throw new MessageNotIntegrityProtectedException(); } @@ -239,24 +257,25 @@ public class OpenPgpMessageInputStream extends DecryptionStream { // Try session key if (options.getSessionKey() != null) { SessionKey sessionKey = options.getSessionKey(); - if (!policy.getSymmetricKeyDecryptionAlgorithmPolicy().isAcceptable(sessionKey.getAlgorithm())) { - throw new UnacceptableAlgorithmException("Symmetric algorithm " + sessionKey.getAlgorithm() + " is not acceptable."); - } + + throwIfUnacceptable(sessionKey.getAlgorithm()); + SessionKeyDataDecryptorFactory decryptorFactory = ImplementationFactory.getInstance() .getSessionKeyDataDecryptorFactory(sessionKey); - // TODO: Replace with encDataList.addSessionKeyDecryptionMethod(sessionKey) - PGPEncryptedData esk = esks.all().get(0); + MessageMetadata.EncryptedData encryptedData = new MessageMetadata.EncryptedData(sessionKey.getAlgorithm()); + try { - MessageMetadata.EncryptedData encryptedData = new MessageMetadata.EncryptedData(sessionKey.getAlgorithm()); + // TODO: Use BCs new API once shipped + PGPEncryptedData esk = esks.all().get(0); if (esk instanceof PGPPBEEncryptedData) { PGPPBEEncryptedData skesk = (PGPPBEEncryptedData) esk; InputStream decrypted = skesk.getDataStream(decryptorFactory); - nestedInputStream = new OpenPgpMessageInputStream(buffer(decrypted), options, encryptedData); + nestedInputStream = new OpenPgpMessageInputStream(buffer(decrypted), options, encryptedData, policy); return true; } else if (esk instanceof PGPPublicKeyEncryptedData) { PGPPublicKeyEncryptedData pkesk = (PGPPublicKeyEncryptedData) esk; InputStream decrypted = pkesk.getDataStream(decryptorFactory); - nestedInputStream = new OpenPgpMessageInputStream(buffer(decrypted), options, encryptedData); + nestedInputStream = new OpenPgpMessageInputStream(buffer(decrypted), options, encryptedData, policy); return true; } else { throw new RuntimeException("Unknown ESK class type: " + esk.getClass().getName()); @@ -268,19 +287,25 @@ public class OpenPgpMessageInputStream extends DecryptionStream { // Try passwords for (PGPPBEEncryptedData skesk : esks.skesks) { + SymmetricKeyAlgorithm kekAlgorithm = SymmetricKeyAlgorithm.requireFromId(skesk.getAlgorithm()); + throwIfUnacceptable(kekAlgorithm); for (Passphrase passphrase : options.getDecryptionPassphrases()) { - PBEDataDecryptorFactory decryptorFactory = ImplementationFactory.getInstance() - .getPBEDataDecryptorFactory(passphrase); - try { - InputStream decrypted = skesk.getDataStream(decryptorFactory); - MessageMetadata.EncryptedData encryptedData = new MessageMetadata.EncryptedData( - SymmetricKeyAlgorithm.requireFromId(skesk.getSymmetricAlgorithm(decryptorFactory))); - nestedInputStream = new OpenPgpMessageInputStream(buffer(decrypted), options, encryptedData); - return true; - } catch (PGPException e) { - // password mismatch? Try next password - } + PBEDataDecryptorFactory decryptorFactory = ImplementationFactory.getInstance() + .getPBEDataDecryptorFactory(passphrase); + try { + InputStream decrypted = skesk.getDataStream(decryptorFactory); + SymmetricKeyAlgorithm sessionKeyAlgorithm = SymmetricKeyAlgorithm.requireFromId( + skesk.getSymmetricAlgorithm(decryptorFactory)); + throwIfUnacceptable(sessionKeyAlgorithm); + MessageMetadata.EncryptedData encryptedData = new MessageMetadata.EncryptedData(sessionKeyAlgorithm); + nestedInputStream = new OpenPgpMessageInputStream(buffer(decrypted), options, encryptedData, policy); + return true; + } catch (UnacceptableAlgorithmException e) { + throw e; + } catch (PGPException e) { + // Password mismatch? + } } } @@ -299,19 +324,17 @@ public class OpenPgpMessageInputStream extends DecryptionStream { .getPublicKeyDataDecryptorFactory(privateKey); try { SymmetricKeyAlgorithm symAlg = SymmetricKeyAlgorithm.requireFromId(pkesk.getSymmetricAlgorithm(decryptorFactory)); - if (!policy.getSymmetricKeyDecryptionAlgorithmPolicy().isAcceptable(symAlg)) { - throw new UnacceptableAlgorithmException("Symmetric-key algorithm " + symAlg + " is not acceptable."); - } + throwIfUnacceptable(symAlg); InputStream decrypted = pkesk.getDataStream(decryptorFactory); MessageMetadata.EncryptedData encryptedData = new MessageMetadata.EncryptedData( SymmetricKeyAlgorithm.requireFromId(pkesk.getSymmetricAlgorithm(decryptorFactory))); - nestedInputStream = new OpenPgpMessageInputStream(buffer(decrypted), options, encryptedData); + nestedInputStream = new OpenPgpMessageInputStream(buffer(decrypted), options, encryptedData, policy); return true; + } catch (UnacceptableAlgorithmException e) { + throw e; } catch (PGPException e) { - if (e instanceof UnacceptableAlgorithmException) { - throw e; - } + } } @@ -327,7 +350,7 @@ public class OpenPgpMessageInputStream extends DecryptionStream { InputStream decrypted = pkesk.getDataStream(decryptorFactory); MessageMetadata.EncryptedData encryptedData = new MessageMetadata.EncryptedData( SymmetricKeyAlgorithm.requireFromId(pkesk.getSymmetricAlgorithm(decryptorFactory))); - nestedInputStream = new OpenPgpMessageInputStream(buffer(decrypted), options, encryptedData); + nestedInputStream = new OpenPgpMessageInputStream(buffer(decrypted), options, encryptedData, policy); return true; } catch (PGPException e) { // hm :/ @@ -339,6 +362,13 @@ public class OpenPgpMessageInputStream extends DecryptionStream { return false; } + private void throwIfUnacceptable(SymmetricKeyAlgorithm algorithm) + throws UnacceptableAlgorithmException { + if (!policy.getSymmetricKeyDecryptionAlgorithmPolicy().isAcceptable(algorithm)) { + throw new UnacceptableAlgorithmException("Symmetric-Key algorithm " + algorithm + " is not acceptable for message decryption."); + } + } + private static InputStream buffer(InputStream inputStream) { return new BufferedInputStream(inputStream); } @@ -370,16 +400,6 @@ public class OpenPgpMessageInputStream extends DecryptionStream { return null; } - private PGPOnePassSignature readOnePassSignature() - throws PGPException, IOException { - return packetInputStream.readOnePassSignature(); - } - - private PGPSignature readSignature() - throws PGPException, IOException { - return packetInputStream.readSignature(); - } - @Override public int read() throws IOException { if (nestedInputStream == null) { @@ -407,7 +427,7 @@ public class OpenPgpMessageInputStream extends DecryptionStream { } catch (PGPException e) { throw new RuntimeException(e); } - signatures.finish(); + signatures.finish(metadata); } return r; } @@ -435,7 +455,7 @@ public class OpenPgpMessageInputStream extends DecryptionStream { } catch (PGPException e) { throw new RuntimeException(e); } - signatures.finish(); + signatures.finish(metadata); } return r; } @@ -517,11 +537,11 @@ public class OpenPgpMessageInputStream extends DecryptionStream { // Furthermore, For 'OPS COMP(LIT("Foo")) SIG', the signature is updated with "Foo". CHAOS!!! private static final class Signatures extends OutputStream { final ConsumerOptions options; - final List detachedSignatures; - final List prependedSignatures; - final List onePassSignatures; - final Stack> opsUpdateStack; - List literalOPS = new ArrayList<>(); + final List detachedSignatures; + final List prependedSignatures; + final List onePassSignatures; + final Stack> opsUpdateStack; + List literalOPS = new ArrayList<>(); final List correspondingSignatures; boolean isLiteral = true; @@ -546,19 +566,19 @@ public class OpenPgpMessageInputStream extends DecryptionStream { long keyId = SignatureUtils.determineIssuerKeyId(signature); PGPPublicKeyRing certificate = findCertificate(keyId); initialize(signature, certificate, keyId); - this.detachedSignatures.add(new SIG(signature, certificate, keyId)); + this.detachedSignatures.add(new DetachedOrPrependedSignature(signature, certificate, keyId)); } void addPrependedSignature(PGPSignature signature) { long keyId = SignatureUtils.determineIssuerKeyId(signature); PGPPublicKeyRing certificate = findCertificate(keyId); initialize(signature, certificate, keyId); - this.prependedSignatures.add(new SIG(signature, certificate, keyId)); + this.prependedSignatures.add(new DetachedOrPrependedSignature(signature, certificate, keyId)); } void addOnePassSignature(PGPOnePassSignature signature) { PGPPublicKeyRing certificate = findCertificate(signature.getKeyID()); - OPS ops = new OPS(signature, certificate, signature.getKeyID()); + OnePassSignature ops = new OnePassSignature(signature, certificate, signature.getKeyID()); ops.init(certificate); onePassSignatures.add(ops); @@ -568,9 +588,9 @@ public class OpenPgpMessageInputStream extends DecryptionStream { } } - void addCorrespondingOnePassSignature(PGPSignature signature) { + void addCorrespondingOnePassSignature(PGPSignature signature, MessageMetadata.Layer layer) { for (int i = onePassSignatures.size() - 1; i >= 0; i--) { - OPS onePassSignature = onePassSignatures.get(i); + OnePassSignature onePassSignature = onePassSignatures.get(i); if (onePassSignature.opSignature.getKeyID() != signature.getKeyID()) { continue; } @@ -579,8 +599,14 @@ public class OpenPgpMessageInputStream extends DecryptionStream { } boolean verified = onePassSignature.verify(signature); - log("One-Pass-Signature by " + Long.toHexString(onePassSignature.opSignature.getKeyID()) + " is " + (verified ? "verified" : "unverified")); - log(onePassSignature.toString()); + SignatureVerification verification = new SignatureVerification(signature, + new SubkeyIdentifier(onePassSignature.certificate, onePassSignature.keyId)); + if (verified) { + layer.addVerifiedSignature(verification); + } else { + layer.addFailedSignature(new SignatureVerification.Failure(verification, + new SignatureValidationException("Incorrect Signature."))); + } break; } } @@ -597,11 +623,7 @@ public class OpenPgpMessageInputStream extends DecryptionStream { opsUpdateStack.pop(); } - private static void initialize(PGPSignature signature, PGPPublicKeyRing certificate, long keyId) { - if (certificate == null) { - // SHIT - return; - } + private static void initialize(@Nonnull PGPSignature signature, @Nonnull PGPPublicKeyRing certificate, long keyId) { PGPContentVerifierBuilderProvider verifierProvider = ImplementationFactory.getInstance() .getPGPContentVerifierBuilderProvider(); try { @@ -611,10 +633,7 @@ public class OpenPgpMessageInputStream extends DecryptionStream { } } - private static void initialize(PGPOnePassSignature ops, PGPPublicKeyRing certificate) { - if (certificate == null) { - return; - } + private static void initialize(@Nonnull PGPOnePassSignature ops, @Nonnull PGPPublicKeyRing certificate) { PGPContentVerifierBuilderProvider verifierProvider = ImplementationFactory.getInstance() .getPGPContentVerifierBuilderProvider(); try { @@ -635,76 +654,74 @@ public class OpenPgpMessageInputStream extends DecryptionStream { } public void updateLiteral(byte b) { - for (OPS ops : literalOPS) { + for (OnePassSignature ops : literalOPS) { ops.update(b); } - for (SIG detached : detachedSignatures) { + for (DetachedOrPrependedSignature detached : detachedSignatures) { detached.update(b); } + + for (DetachedOrPrependedSignature prepended : prependedSignatures) { + prepended.update(b); + } } public void updateLiteral(byte[] b, int off, int len) { - for (OPS ops : literalOPS) { + for (OnePassSignature ops : literalOPS) { ops.update(b, off, len); } - for (SIG detached : detachedSignatures) { + for (DetachedOrPrependedSignature detached : detachedSignatures) { detached.update(b, off, len); } + + for (DetachedOrPrependedSignature prepended : prependedSignatures) { + prepended.update(b, off, len); + } } public void updatePacket(byte b) { - for (SIG detached : detachedSignatures) { - detached.update(b); - } - - for (SIG prepended : prependedSignatures) { - prepended.update(b); - } - for (int i = opsUpdateStack.size() - 1; i >= 0; i--) { - List nestedOPSs = opsUpdateStack.get(i); - for (OPS ops : nestedOPSs) { + List nestedOPSs = opsUpdateStack.get(i); + for (OnePassSignature ops : nestedOPSs) { ops.update(b); } } } public void updatePacket(byte[] buf, int off, int len) { - for (SIG detached : detachedSignatures) { - detached.update(buf, off, len); - } - - for (SIG prepended : prependedSignatures) { - prepended.update(buf, off, len); - } - for (int i = opsUpdateStack.size() - 1; i >= 0; i--) { - List nestedOPSs = opsUpdateStack.get(i); - for (OPS ops : nestedOPSs) { + List nestedOPSs = opsUpdateStack.get(i); + for (OnePassSignature ops : nestedOPSs) { ops.update(buf, off, len); } } } - public void finish() { - for (SIG detached : detachedSignatures) { + public void finish(MessageMetadata.Layer layer) { + for (DetachedOrPrependedSignature detached : detachedSignatures) { boolean verified = detached.verify(); + SignatureVerification verification = new SignatureVerification( + detached.signature, new SubkeyIdentifier(detached.certificate, detached.keyId)); if (verified) { - this.verified.add(detached.signature); + layer.addVerifiedSignature(verification); + } else { + layer.addFailedSignature(new SignatureVerification.Failure( + verification, new SignatureValidationException("Incorrect Signature."))); } - log("Detached Signature by " + Long.toHexString(detached.signature.getKeyID()) + " is " + (verified ? "verified" : "unverified")); - log(detached.toString()); } - for (SIG prepended : prependedSignatures) { + for (DetachedOrPrependedSignature prepended : prependedSignatures) { boolean verified = prepended.verify(); + SignatureVerification verification = new SignatureVerification( + prepended.signature, new SubkeyIdentifier(prepended.certificate, prepended.keyId)); if (verified) { - this.verified.add(prepended.signature); + layer.addVerifiedSignature(verification); + } else { + layer.addFailedSignature(new SignatureVerification.Failure( + verification, new SignatureValidationException("Incorrect Signature."))); } - log("Prepended Signature by " + Long.toHexString(prepended.signature.getKeyID()) + " is " + (verified ? "verified" : "unverified")); - log(prepended.toString()); } } @@ -729,7 +746,7 @@ public class OpenPgpMessageInputStream extends DecryptionStream { } } - static class SIG { + static class DetachedOrPrependedSignature { ByteArrayOutputStream bytes = new ByteArrayOutputStream(); PGPSignature signature; PGPPublicKeyRing certificate; @@ -737,7 +754,7 @@ public class OpenPgpMessageInputStream extends DecryptionStream { boolean finished; boolean valid; - public SIG(PGPSignature signature, PGPPublicKeyRing certificate, long keyId) { + public DetachedOrPrependedSignature(PGPSignature signature, PGPPublicKeyRing certificate, long keyId) { this.signature = signature; this.certificate = certificate; this.keyId = keyId; @@ -762,8 +779,7 @@ public class OpenPgpMessageInputStream extends DecryptionStream { public void update(byte b) { if (finished) { - log("Updating finished sig!"); - return; + throw new IllegalStateException("Already finished."); } signature.update(b); bytes.write(b); @@ -771,52 +787,14 @@ public class OpenPgpMessageInputStream extends DecryptionStream { public void update(byte[] bytes, int off, int len) { if (finished) { - log("Updating finished sig!"); - return; + throw new IllegalStateException("Already finished."); } signature.update(bytes, off, len); this.bytes.write(bytes, off, len); } - - @Override - public String toString() { - String OPS = "c40d03000a01fbfcc82a015e733001"; - String LIT_H = "cb28620000000000"; - String LIT = "656e637279707420e28898207369676e20e28898207369676e20e28898207369676e"; - String SIG1 = "c2c10400010a006f058262c806350910fbfcc82a015e7330471400000000001e002073616c74406e6f746174696f6e732e736571756f69612d7067702e6f7267b0409ed8ea96dac66447bdff5b7b60c9f80a0ab91d257029153dc3b6d8c27b98162104d1a66e1a23b182c9980f788cfbfcc82a015e7330000029640c00846b5096d92474fd446cc7edaf9f14572cab93a80e12384c1e829f95debc6e8373c2ce5402be53dc1a18cf92a0ed909e0fb38855713ef8ffb13502ffac7c830fa254cc1aa6c666a97b0cc3bc176538f6913d3b8e8981a65cc42df10e0f39e4d0a06dfe961437b59a71892f4fca1116aed15123ea0d86a7b2ce47dd9d3ef22d920631bc011e82babe03ad5d72b3ba7f95bf646f20ccf6f7a4d95de37397c76c7d53741458e51ab6074007f61181c7b88b7c98f5b7510c8dfa3be01f4841501679478b15c5249d928e2a10d15ec63efa1500b994d5bfb32ffb174a976116930eb97a111e6dfd4c5e43e04a5d76ba74806a62fda63a8c3f53f6eebaf852892340e81dd08bbf348454a2cf525aeb512cf33aeeee78465ee4c442e41cc45ac4e3bb0c3333677aa60332ee7f464d9020f8554b82d619872477cca18d8431888f4ae8abe5894e9720f759c410cd7991db12703dc147040dd0d3758223e0b75de6ceae49c1a0c2c45efedeb7114ae785cc886afdc45c82172e4476e1ab5b86dc4314dd76"; - String SIG1f = "c2c13b0400010a006f058262c806350910fbfcc82a015e7330471400000000001e002073616c74406e6f746174696f6e732e736571756f69612d7067702e6f7267b0409ed8ea96dac66447bdff5b7b60c9f80a0ab91d257029153dc3b6d8c27b98162104d1a66e1a23b182c9980f788cfbfcc82a015e7330000029640c00846b5096d92474fd446cc7edaf9f14572cab93a80e12384c1e829f95debc6e8373c2ce5402be53dc1a18cf92a0ed909e0fb38855713ef8ffb13502ffac7c830fa254cc1aa6c666a97b0cc3bc176538f6913d3b8e8981a65cc42df10e0f39e4d0a06dfe961437b59a71892f4fca1116aed15123ea0d86a7b2ce47dd9d3ef22d920631bc011e82babe03ad5d72b3ba7f95bf646f20ccf6f7a4d95de37397c76c7d53741458e51ab6074007f61181c7b88b7c98f5b7510c8dfa3be01f4841501679478b15c5249d928e2a10d15ec63efa1500b994d5bfb32ffb174a976116930eb97a111e6dfd4c5e43e04a5d76ba74806a62fda63a8c3f53f6eebaf852892340e81dd08bbf348454a2cf525aeb512cf33aeeee78465ee4c442e41cc45ac4e3bb0c3333677aa60332ee7f464d9020f8554b82d619872477cca18d8431888f4ae8abe5894e9720f759c410cd7991db12703dc147040dd0d3758223e0b75de6ceae49c1a0c2c45efedeb7114ae785cc886afdc45c82172e4476e1ab5b86dc4314dd76"; - String SIG2 = "c2c10400010a006f058262c806350910fbfcc82a015e7330471400000000001e002073616c74406e6f746174696f6e732e736571756f69612d7067702e6f7267a4d9c117dc7ba3a7e9270856f128d2ab271743eac3cb5750b22a89bd5fd60753162104d1a66e1a23b182c9980f788cfbfcc82a015e73300000b8400bff796c20fa8b25ff7a42686338e06417a2966e85a0fc2723c928bef6cd19d34cf5e7d55ada33080613012dadb79e0278e59d9e7ed7d2d6102912a5f768c2e75b60099225c3d8bfe0c123240188b80dbee89b9b3bd5b13ccc662abc37e2129b6968adac9aba43aa778c0fe4fe337591ee87a96a29a013debc83555293c877144fc676aa1b03782c501949521a320adf6ad96c4f2e036b52a18369c637fdc49033696a84d03a69580b953187fce5aca6fb26fc8815da9f3b513bfe8e304f33ecb4b521aeb7d09c4a284ea66123bd0d6a358b2526d762ca110e1f7f20b3038d774b64d5cfd34e2213765828359d7afc5bf24d5270e99d80c3c1568fa01624b6ea1e9ce4e6890ce9bacf6611a45d41e2671f68f5b096446bf08d27ce75608425b2e3ab92146229ad1fcd8224aca5b5f73960506e7df07bfbf3664348e8ecbfb2eb467b9cfe412cb377a6ee2eb5fd11be9cf9208fe9a74c296f52cfa02a1eb0519ad9a8349bf6ccd6495feb7e391451bf96e08a0798883dee5974e47cbf3b51f111b6d3"; - String SIG2f = "c2c13b0400010a006f058262c806350910fbfcc82a015e7330471400000000001e002073616c74406e6f746174696f6e732e736571756f69612d7067702e6f7267a4d9c117dc7ba3a7e9270856f128d2ab271743eac3cb5750b22a89bd5fd60753162104d1a66e1a23b182c9980f788cfbfcc82a015e73300000b8400bff796c20fa8b25ff7a42686338e06417a2966e85a0fc2723c928bef6cd19d34cf5e7d55ada33080613012dadb79e0278e59d9e7ed7d2d6102912a5f768c2e75b60099225c3d8bfe0c123240188b80dbee89b9b3bd5b13ccc662abc37e2129b6968adac9aba43aa778c0fe4fe337591ee87a96a29a013debc83555293c877144fc676aa1b03782c501949521a320adf6ad96c4f2e036b52a18369c637fdc49033696a84d03a69580b953187fce5aca6fb26fc8815da9f3b513bfe8e304f33ecb4b521aeb7d09c4a284ea66123bd0d6a358b2526d762ca110e1f7f20b3038d774b64d5cfd34e2213765828359d7afc5bf24d5270e99d80c3c1568fa01624b6ea1e9ce4e6890ce9bacf6611a45d41e2671f68f5b096446bf08d27ce75608425b2e3ab92146229ad1fcd8224aca5b5f73960506e7df07bfbf3664348e8ecbfb2eb467b9cfe412cb377a6ee2eb5fd11be9cf9208fe9a74c296f52cfa02a1eb0519ad9a8349bf6ccd6495feb7e391451bf96e08a0798883dee5974e47cbf3b51f111b6d3"; - String out = ""; - - String hex = Hex.toHexString(bytes.toByteArray()); - while (hex.contains(OPS)) { - hex = hex.replace(OPS, "[OPS]"); - } - while (hex.contains(LIT_H)) { - hex = hex.replace(LIT_H, "[LIT]"); - } - while (hex.contains(LIT)) { - hex = hex.replace(LIT, ""); - } - while (hex.contains(SIG1)) { - hex = hex.replace(SIG1, "[SIG1]"); - } - while (hex.contains(SIG1f)) { - hex = hex.replace(SIG1f, "[SIG1f]"); - } - while (hex.contains(SIG2)) { - hex = hex.replace(SIG2, "[SIG2]"); - } - while (hex.contains(SIG2f)) { - hex = hex.replace(SIG2f, "[SIG2f]"); - } - - return out + hex; - } } - static class OPS { + static class OnePassSignature { ByteArrayOutputStream bytes = new ByteArrayOutputStream(); PGPOnePassSignature opSignature; PGPSignature signature; @@ -825,7 +803,7 @@ public class OpenPgpMessageInputStream extends DecryptionStream { boolean finished; boolean valid; - public OPS(PGPOnePassSignature signature, PGPPublicKeyRing certificate, long keyId) { + public OnePassSignature(PGPOnePassSignature signature, PGPPublicKeyRing certificate, long keyId) { this.opSignature = signature; this.certificate = certificate; this.keyId = keyId; @@ -836,6 +814,10 @@ public class OpenPgpMessageInputStream extends DecryptionStream { } public boolean verify(PGPSignature signature) { + if (finished) { + throw new IllegalStateException("Already finished."); + } + if (this.opSignature.getKeyID() != signature.getKeyID()) { // nope return false; @@ -852,8 +834,7 @@ public class OpenPgpMessageInputStream extends DecryptionStream { public void update(byte b) { if (finished) { - log("Updating finished sig!"); - return; + throw new IllegalStateException("Already finished."); } opSignature.update(b); bytes.write(b); @@ -861,49 +842,11 @@ public class OpenPgpMessageInputStream extends DecryptionStream { public void update(byte[] bytes, int off, int len) { if (finished) { - log("Updating finished sig!"); - return; + throw new IllegalStateException("Already finished."); } opSignature.update(bytes, off, len); this.bytes.write(bytes, off, len); } - - @Override - public String toString() { - String OPS = "c40d03000a01fbfcc82a015e733001"; - String LIT_H = "cb28620000000000"; - String LIT = "656e637279707420e28898207369676e20e28898207369676e20e28898207369676e"; - String SIG1 = "c2c10400010a006f058262c806350910fbfcc82a015e7330471400000000001e002073616c74406e6f746174696f6e732e736571756f69612d7067702e6f7267b0409ed8ea96dac66447bdff5b7b60c9f80a0ab91d257029153dc3b6d8c27b98162104d1a66e1a23b182c9980f788cfbfcc82a015e7330000029640c00846b5096d92474fd446cc7edaf9f14572cab93a80e12384c1e829f95debc6e8373c2ce5402be53dc1a18cf92a0ed909e0fb38855713ef8ffb13502ffac7c830fa254cc1aa6c666a97b0cc3bc176538f6913d3b8e8981a65cc42df10e0f39e4d0a06dfe961437b59a71892f4fca1116aed15123ea0d86a7b2ce47dd9d3ef22d920631bc011e82babe03ad5d72b3ba7f95bf646f20ccf6f7a4d95de37397c76c7d53741458e51ab6074007f61181c7b88b7c98f5b7510c8dfa3be01f4841501679478b15c5249d928e2a10d15ec63efa1500b994d5bfb32ffb174a976116930eb97a111e6dfd4c5e43e04a5d76ba74806a62fda63a8c3f53f6eebaf852892340e81dd08bbf348454a2cf525aeb512cf33aeeee78465ee4c442e41cc45ac4e3bb0c3333677aa60332ee7f464d9020f8554b82d619872477cca18d8431888f4ae8abe5894e9720f759c410cd7991db12703dc147040dd0d3758223e0b75de6ceae49c1a0c2c45efedeb7114ae785cc886afdc45c82172e4476e1ab5b86dc4314dd76"; - String SIG1f = "c2c13b0400010a006f058262c806350910fbfcc82a015e7330471400000000001e002073616c74406e6f746174696f6e732e736571756f69612d7067702e6f7267b0409ed8ea96dac66447bdff5b7b60c9f80a0ab91d257029153dc3b6d8c27b98162104d1a66e1a23b182c9980f788cfbfcc82a015e7330000029640c00846b5096d92474fd446cc7edaf9f14572cab93a80e12384c1e829f95debc6e8373c2ce5402be53dc1a18cf92a0ed909e0fb38855713ef8ffb13502ffac7c830fa254cc1aa6c666a97b0cc3bc176538f6913d3b8e8981a65cc42df10e0f39e4d0a06dfe961437b59a71892f4fca1116aed15123ea0d86a7b2ce47dd9d3ef22d920631bc011e82babe03ad5d72b3ba7f95bf646f20ccf6f7a4d95de37397c76c7d53741458e51ab6074007f61181c7b88b7c98f5b7510c8dfa3be01f4841501679478b15c5249d928e2a10d15ec63efa1500b994d5bfb32ffb174a976116930eb97a111e6dfd4c5e43e04a5d76ba74806a62fda63a8c3f53f6eebaf852892340e81dd08bbf348454a2cf525aeb512cf33aeeee78465ee4c442e41cc45ac4e3bb0c3333677aa60332ee7f464d9020f8554b82d619872477cca18d8431888f4ae8abe5894e9720f759c410cd7991db12703dc147040dd0d3758223e0b75de6ceae49c1a0c2c45efedeb7114ae785cc886afdc45c82172e4476e1ab5b86dc4314dd76"; - String SIG2 = "c2c10400010a006f058262c806350910fbfcc82a015e7330471400000000001e002073616c74406e6f746174696f6e732e736571756f69612d7067702e6f7267a4d9c117dc7ba3a7e9270856f128d2ab271743eac3cb5750b22a89bd5fd60753162104d1a66e1a23b182c9980f788cfbfcc82a015e73300000b8400bff796c20fa8b25ff7a42686338e06417a2966e85a0fc2723c928bef6cd19d34cf5e7d55ada33080613012dadb79e0278e59d9e7ed7d2d6102912a5f768c2e75b60099225c3d8bfe0c123240188b80dbee89b9b3bd5b13ccc662abc37e2129b6968adac9aba43aa778c0fe4fe337591ee87a96a29a013debc83555293c877144fc676aa1b03782c501949521a320adf6ad96c4f2e036b52a18369c637fdc49033696a84d03a69580b953187fce5aca6fb26fc8815da9f3b513bfe8e304f33ecb4b521aeb7d09c4a284ea66123bd0d6a358b2526d762ca110e1f7f20b3038d774b64d5cfd34e2213765828359d7afc5bf24d5270e99d80c3c1568fa01624b6ea1e9ce4e6890ce9bacf6611a45d41e2671f68f5b096446bf08d27ce75608425b2e3ab92146229ad1fcd8224aca5b5f73960506e7df07bfbf3664348e8ecbfb2eb467b9cfe412cb377a6ee2eb5fd11be9cf9208fe9a74c296f52cfa02a1eb0519ad9a8349bf6ccd6495feb7e391451bf96e08a0798883dee5974e47cbf3b51f111b6d3"; - String SIG2f = "c2c13b0400010a006f058262c806350910fbfcc82a015e7330471400000000001e002073616c74406e6f746174696f6e732e736571756f69612d7067702e6f7267a4d9c117dc7ba3a7e9270856f128d2ab271743eac3cb5750b22a89bd5fd60753162104d1a66e1a23b182c9980f788cfbfcc82a015e73300000b8400bff796c20fa8b25ff7a42686338e06417a2966e85a0fc2723c928bef6cd19d34cf5e7d55ada33080613012dadb79e0278e59d9e7ed7d2d6102912a5f768c2e75b60099225c3d8bfe0c123240188b80dbee89b9b3bd5b13ccc662abc37e2129b6968adac9aba43aa778c0fe4fe337591ee87a96a29a013debc83555293c877144fc676aa1b03782c501949521a320adf6ad96c4f2e036b52a18369c637fdc49033696a84d03a69580b953187fce5aca6fb26fc8815da9f3b513bfe8e304f33ecb4b521aeb7d09c4a284ea66123bd0d6a358b2526d762ca110e1f7f20b3038d774b64d5cfd34e2213765828359d7afc5bf24d5270e99d80c3c1568fa01624b6ea1e9ce4e6890ce9bacf6611a45d41e2671f68f5b096446bf08d27ce75608425b2e3ab92146229ad1fcd8224aca5b5f73960506e7df07bfbf3664348e8ecbfb2eb467b9cfe412cb377a6ee2eb5fd11be9cf9208fe9a74c296f52cfa02a1eb0519ad9a8349bf6ccd6495feb7e391451bf96e08a0798883dee5974e47cbf3b51f111b6d3"; - String out = "last=" + opSignature.isContaining() + "\n"; - - String hex = Hex.toHexString(bytes.toByteArray()); - while (hex.contains(OPS)) { - hex = hex.replace(OPS, "[OPS]"); - } - while (hex.contains(LIT_H)) { - hex = hex.replace(LIT_H, "[LIT]"); - } - while (hex.contains(LIT)) { - hex = hex.replace(LIT, ""); - } - while (hex.contains(SIG1)) { - hex = hex.replace(SIG1, "[SIG1]"); - } - while (hex.contains(SIG1f)) { - hex = hex.replace(SIG1f, "[SIG1f]"); - } - while (hex.contains(SIG2)) { - hex = hex.replace(SIG2, "[SIG2]"); - } - while (hex.contains(SIG2f)) { - hex = hex.replace(SIG2f, "[SIG2f]"); - } - - return out + hex; - } } } @@ -916,7 +859,7 @@ public class OpenPgpMessageInputStream extends DecryptionStream { resultBuilder.setFileEncoding(m.getFormat()); resultBuilder.setSessionKey(m.getSessionKey()); - for (Signatures.OPS ops : signatures.onePassSignatures) { + for (Signatures.OnePassSignature ops : signatures.onePassSignatures) { if (!ops.finished) { continue; } @@ -930,7 +873,7 @@ public class OpenPgpMessageInputStream extends DecryptionStream { } } - for (Signatures.SIG prep : signatures.prependedSignatures) { + for (Signatures.DetachedOrPrependedSignature prep : signatures.prependedSignatures) { if (!prep.finished) { continue; } @@ -944,7 +887,7 @@ public class OpenPgpMessageInputStream extends DecryptionStream { } } - for (Signatures.SIG det : signatures.detachedSignatures) { + for (Signatures.DetachedOrPrependedSignature det : signatures.detachedSignatures) { if (!det.finished) { continue; } @@ -964,7 +907,7 @@ public class OpenPgpMessageInputStream extends DecryptionStream { static void log(String message) { LOGGER.debug(message); // CHECKSTYLE:OFF - // System.out.println(message); + System.out.println(message); // CHECKSTYLE:ON } diff --git a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStreamTest.java b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStreamTest.java index 1955eebe..87ae27f8 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStreamTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStreamTest.java @@ -4,6 +4,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; @@ -331,6 +332,8 @@ public class OpenPgpMessageInputStreamTest { assertEquals("", metadata.getFilename()); JUtils.assertDateEquals(new Date(0L), metadata.getModificationDate()); assertEquals(StreamEncoding.BINARY, metadata.getFormat()); + assertTrue(metadata.getVerifiedSignatures().isEmpty()); + assertTrue(metadata.getRejectedSignatures().isEmpty()); } @ParameterizedTest(name = "Process LIT LIT using {0}") @@ -349,6 +352,8 @@ public class OpenPgpMessageInputStreamTest { assertEquals(PLAINTEXT, plain); MessageMetadata metadata = result.getB(); assertEquals(CompressionAlgorithm.ZIP, metadata.getCompressionAlgorithm()); + assertTrue(metadata.getVerifiedSignatures().isEmpty()); + assertTrue(metadata.getRejectedSignatures().isEmpty()); } @ParameterizedTest(name = "Process COMP using {0}") @@ -372,6 +377,8 @@ public class OpenPgpMessageInputStreamTest { assertEquals(CompressionAlgorithm.BZIP2, compressionAlgorithms.next()); assertFalse(compressionAlgorithms.hasNext()); assertNull(metadata.getEncryptionAlgorithm()); + assertTrue(metadata.getVerifiedSignatures().isEmpty()); + assertTrue(metadata.getRejectedSignatures().isEmpty()); } @ParameterizedTest(name = "Process SIG COMP(LIT) using {0}") @@ -388,6 +395,8 @@ public class OpenPgpMessageInputStreamTest { MessageMetadata metadata = result.getB(); assertEquals(CompressionAlgorithm.ZIP, metadata.getCompressionAlgorithm()); assertNull(metadata.getEncryptionAlgorithm()); + assertFalse(metadata.getVerifiedSignatures().isEmpty()); + assertTrue(metadata.getRejectedSignatures().isEmpty()); } @ParameterizedTest(name = "Process SENC(LIT) using {0}") @@ -401,6 +410,8 @@ public class OpenPgpMessageInputStreamTest { MessageMetadata metadata = result.getB(); assertNull(metadata.getCompressionAlgorithm()); assertEquals(SymmetricKeyAlgorithm.AES_256, metadata.getEncryptionAlgorithm()); + assertTrue(metadata.getVerifiedSignatures().isEmpty()); + assertTrue(metadata.getRejectedSignatures().isEmpty()); } @ParameterizedTest(name = "Process PENC(COMP(LIT)) using {0}") @@ -415,6 +426,8 @@ public class OpenPgpMessageInputStreamTest { MessageMetadata metadata = result.getB(); assertEquals(CompressionAlgorithm.ZLIB, metadata.getCompressionAlgorithm()); assertEquals(SymmetricKeyAlgorithm.AES_256, metadata.getEncryptionAlgorithm()); + assertTrue(metadata.getVerifiedSignatures().isEmpty()); + assertTrue(metadata.getRejectedSignatures().isEmpty()); } @ParameterizedTest(name = "Process OPS LIT SIG using {0}") @@ -429,6 +442,8 @@ public class OpenPgpMessageInputStreamTest { MessageMetadata metadata = result.getB(); assertNull(metadata.getEncryptionAlgorithm()); assertNull(metadata.getCompressionAlgorithm()); + assertFalse(metadata.getVerifiedSignatures().isEmpty()); + assertTrue(metadata.getRejectedSignatures().isEmpty()); } String BOB_KEY = "-----BEGIN PGP PRIVATE KEY BLOCK-----\n" + @@ -564,6 +579,8 @@ public class OpenPgpMessageInputStreamTest { MessageMetadata metadata = result.getB(); assertEquals(SymmetricKeyAlgorithm.AES_256, metadata.getEncryptionAlgorithm()); assertNull(metadata.getCompressionAlgorithm()); + assertFalse(metadata.getVerifiedSignatures().isEmpty()); + assertTrue(metadata.getRejectedSignatures().isEmpty()); } @ParameterizedTest(name = "Process PENC(OPS OPS OPS LIT SIG SIG SIG) using {0}") @@ -627,6 +644,8 @@ public class OpenPgpMessageInputStreamTest { MessageMetadata metadata = result.getB(); assertEquals(SymmetricKeyAlgorithm.AES_256, metadata.getEncryptionAlgorithm()); assertNull(metadata.getCompressionAlgorithm()); + assertFalse(metadata.getVerifiedSignatures().isEmpty()); + assertTrue(metadata.getRejectedSignatures().isEmpty()); } private static Tuple processReadBuffered(String armoredMessage, ConsumerOptions options) From 654493dfccb2e16f3327c98c402d82757132f0ca Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Sun, 16 Oct 2022 18:54:22 +0200 Subject: [PATCH 041/763] Properly expose signatures --- .../MessageMetadata.java | 87 ++++++++-- .../OpenPgpMessageInputStream.java | 162 +++++++++--------- .../OpenPgpMessageInputStreamTest.java | 36 ++-- 3 files changed, 171 insertions(+), 114 deletions(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/MessageMetadata.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/MessageMetadata.java index bb2f5c76..2cd2a6f2 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/MessageMetadata.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/MessageMetadata.java @@ -91,16 +91,24 @@ public class MessageMetadata { }; } - public @Nonnull List getVerifiedSignatures() { + public List getVerifiedDetachedSignatures() { + return new ArrayList<>(message.verifiedDetachedSignatures); + } + + public List getRejectedDetachedSignatures() { + return new ArrayList<>(message.rejectedDetachedSignatures); + } + + public @Nonnull List getVerifiedInlineSignatures() { List verifications = new ArrayList<>(); - Iterator> verificationsByLayer = getVerifiedSignaturesByLayer(); + Iterator> verificationsByLayer = getVerifiedInlineSignaturesByLayer(); while (verificationsByLayer.hasNext()) { verifications.addAll(verificationsByLayer.next()); } return verifications; } - public @Nonnull Iterator> getVerifiedSignaturesByLayer() { + public @Nonnull Iterator> getVerifiedInlineSignaturesByLayer() { return new LayerIterator>(message) { @Override boolean matches(Nested layer) { @@ -114,21 +122,24 @@ public class MessageMetadata { @Override List getProperty(Layer last) { - return new ArrayList<>(last.getVerifiedSignatures()); + List list = new ArrayList<>(); + list.addAll(last.getVerifiedOnePassSignatures()); + list.addAll(last.getVerifiedPrependedSignatures()); + return list; } }; } - public @Nonnull List getRejectedSignatures() { + public @Nonnull List getRejectedInlineSignatures() { List rejected = new ArrayList<>(); - Iterator> rejectedByLayer = getRejectedSignaturesByLayer(); + Iterator> rejectedByLayer = getRejectedInlineSignaturesByLayer(); while (rejectedByLayer.hasNext()) { rejected.addAll(rejectedByLayer.next()); } return rejected; } - public @Nonnull Iterator> getRejectedSignaturesByLayer() { + public @Nonnull Iterator> getRejectedInlineSignaturesByLayer() { return new LayerIterator>(message) { @Override boolean matches(Nested layer) { @@ -142,7 +153,10 @@ public class MessageMetadata { @Override List getProperty(Layer last) { - return new ArrayList<>(last.getFailedSignatures()); + List list = new ArrayList<>(); + list.addAll(last.getRejectedOnePassSignatures()); + list.addAll(last.getRejectedPrependedSignatures()); + return list; } }; } @@ -169,8 +183,12 @@ public class MessageMetadata { } public abstract static class Layer { - protected final List verifiedSignatures = new ArrayList<>(); - protected final List failedSignatures = new ArrayList<>(); + protected final List verifiedDetachedSignatures = new ArrayList<>(); + protected final List rejectedDetachedSignatures = new ArrayList<>(); + protected final List verifiedOnePassSignatures = new ArrayList<>(); + protected final List rejectedOnePassSignatures = new ArrayList<>(); + protected final List verifiedPrependedSignatures = new ArrayList<>(); + protected final List rejectedPrependedSignatures = new ArrayList<>(); protected Nested child; public Nested getChild() { @@ -181,21 +199,54 @@ public class MessageMetadata { this.child = child; } - public List getVerifiedSignatures() { - return new ArrayList<>(verifiedSignatures); + public List getVerifiedDetachedSignatures() { + return new ArrayList<>(verifiedDetachedSignatures); } - public List getFailedSignatures() { - return new ArrayList<>(failedSignatures); + public List getRejectedDetachedSignatures() { + return new ArrayList<>(rejectedDetachedSignatures); } - void addVerifiedSignature(SignatureVerification signatureVerification) { - verifiedSignatures.add(signatureVerification); + void addVerifiedDetachedSignature(SignatureVerification signatureVerification) { + verifiedDetachedSignatures.add(signatureVerification); } - void addFailedSignature(SignatureVerification.Failure failure) { - failedSignatures.add(failure); + void addRejectedDetachedSignature(SignatureVerification.Failure failure) { + rejectedDetachedSignatures.add(failure); } + + public List getVerifiedOnePassSignatures() { + return new ArrayList<>(verifiedOnePassSignatures); + } + + public List getRejectedOnePassSignatures() { + return new ArrayList<>(rejectedOnePassSignatures); + } + + void addVerifiedOnePassSignature(SignatureVerification verifiedOnePassSignature) { + this.verifiedOnePassSignatures.add(verifiedOnePassSignature); + } + + void addRejectedOnePassSignature(SignatureVerification.Failure rejected) { + this.rejectedOnePassSignatures.add(rejected); + } + + public List getVerifiedPrependedSignatures() { + return new ArrayList<>(verifiedPrependedSignatures); + } + + public List getRejectedPrependedSignatures() { + return new ArrayList<>(rejectedPrependedSignatures); + } + + void addVerifiedPrependedSignature(SignatureVerification verified) { + this.verifiedPrependedSignatures.add(verified); + } + + void addRejectedPrependedSignature(SignatureVerification.Failure rejected) { + this.rejectedPrependedSignatures.add(rejected); + } + } public interface Nested { diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java index 9e26dc20..a4b05bbe 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java @@ -29,6 +29,7 @@ import org.bouncycastle.openpgp.PGPPublicKeyEncryptedData; import org.bouncycastle.openpgp.PGPPublicKeyRing; import org.bouncycastle.openpgp.PGPSecretKey; import org.bouncycastle.openpgp.PGPSecretKeyRing; +import org.bouncycastle.openpgp.PGPSessionKey; import org.bouncycastle.openpgp.PGPSignature; import org.bouncycastle.openpgp.operator.PBEDataDecryptorFactory; import org.bouncycastle.openpgp.operator.PGPContentVerifierBuilderProvider; @@ -55,6 +56,7 @@ import org.pgpainless.key.protection.SecretKeyRingProtector; import org.pgpainless.key.protection.UnlockSecretKey; import org.pgpainless.policy.Policy; import org.pgpainless.signature.SignatureUtils; +import org.pgpainless.signature.consumer.SignatureValidator; import org.pgpainless.util.Passphrase; import org.pgpainless.util.SessionKey; import org.pgpainless.util.Tuple; @@ -94,9 +96,9 @@ public class OpenPgpMessageInputStream extends DecryptionStream { } protected OpenPgpMessageInputStream(@Nonnull InputStream inputStream, - @Nonnull ConsumerOptions options, - @Nonnull MessageMetadata.Layer metadata, - @Nonnull Policy policy) + @Nonnull ConsumerOptions options, + @Nonnull MessageMetadata.Layer metadata, + @Nonnull Policy policy) throws PGPException, IOException { super(OpenPgpMetadata.getBuilder()); @@ -173,7 +175,7 @@ public class OpenPgpMessageInputStream extends DecryptionStream { throw new MissingDecryptionMethodException("No working decryption method found."); - // Marker Packets need to be skipped and ignored + // Marker Packets need to be skipped and ignored case MARKER: packetInputStream.readMarker(); break; @@ -236,7 +238,7 @@ public class OpenPgpMessageInputStream extends DecryptionStream { PGPSignature signature = packetInputStream.readSignature(); if (isSigForOPS) { signatures.leaveNesting(); // TODO: Only leave nesting if all OPSs of the nesting layer are dealt with - signatures.addCorrespondingOnePassSignature(signature, metadata); + signatures.addCorrespondingOnePassSignature(signature, metadata, policy); } else { signatures.addPrependedSignature(signature); } @@ -257,7 +259,6 @@ public class OpenPgpMessageInputStream extends DecryptionStream { // Try session key if (options.getSessionKey() != null) { SessionKey sessionKey = options.getSessionKey(); - throwIfUnacceptable(sessionKey.getAlgorithm()); SessionKeyDataDecryptorFactory decryptorFactory = ImplementationFactory.getInstance() @@ -270,11 +271,13 @@ public class OpenPgpMessageInputStream extends DecryptionStream { if (esk instanceof PGPPBEEncryptedData) { PGPPBEEncryptedData skesk = (PGPPBEEncryptedData) esk; InputStream decrypted = skesk.getDataStream(decryptorFactory); + encryptedData.sessionKey = sessionKey; nestedInputStream = new OpenPgpMessageInputStream(buffer(decrypted), options, encryptedData, policy); return true; } else if (esk instanceof PGPPublicKeyEncryptedData) { PGPPublicKeyEncryptedData pkesk = (PGPPublicKeyEncryptedData) esk; InputStream decrypted = pkesk.getDataStream(decryptorFactory); + encryptedData.sessionKey = sessionKey; nestedInputStream = new OpenPgpMessageInputStream(buffer(decrypted), options, encryptedData, policy); return true; } else { @@ -290,22 +293,22 @@ public class OpenPgpMessageInputStream extends DecryptionStream { SymmetricKeyAlgorithm kekAlgorithm = SymmetricKeyAlgorithm.requireFromId(skesk.getAlgorithm()); throwIfUnacceptable(kekAlgorithm); for (Passphrase passphrase : options.getDecryptionPassphrases()) { - PBEDataDecryptorFactory decryptorFactory = ImplementationFactory.getInstance() - .getPBEDataDecryptorFactory(passphrase); + PBEDataDecryptorFactory decryptorFactory = ImplementationFactory.getInstance() + .getPBEDataDecryptorFactory(passphrase); - try { - InputStream decrypted = skesk.getDataStream(decryptorFactory); - SymmetricKeyAlgorithm sessionKeyAlgorithm = SymmetricKeyAlgorithm.requireFromId( - skesk.getSymmetricAlgorithm(decryptorFactory)); - throwIfUnacceptable(sessionKeyAlgorithm); - MessageMetadata.EncryptedData encryptedData = new MessageMetadata.EncryptedData(sessionKeyAlgorithm); - nestedInputStream = new OpenPgpMessageInputStream(buffer(decrypted), options, encryptedData, policy); - return true; - } catch (UnacceptableAlgorithmException e) { - throw e; - } catch (PGPException e) { - // Password mismatch? - } + try { + InputStream decrypted = skesk.getDataStream(decryptorFactory); + SessionKey sessionKey = new SessionKey(skesk.getSessionKey(decryptorFactory)); + throwIfUnacceptable(sessionKey.getAlgorithm()); + MessageMetadata.EncryptedData encryptedData = new MessageMetadata.EncryptedData(sessionKey.getAlgorithm()); + encryptedData.sessionKey = sessionKey; + nestedInputStream = new OpenPgpMessageInputStream(buffer(decrypted), options, encryptedData, policy); + return true; + } catch (UnacceptableAlgorithmException e) { + throw e; + } catch (PGPException e) { + // Password mismatch? + } } } @@ -323,11 +326,13 @@ public class OpenPgpMessageInputStream extends DecryptionStream { PublicKeyDataDecryptorFactory decryptorFactory = ImplementationFactory.getInstance() .getPublicKeyDataDecryptorFactory(privateKey); try { - SymmetricKeyAlgorithm symAlg = SymmetricKeyAlgorithm.requireFromId(pkesk.getSymmetricAlgorithm(decryptorFactory)); - throwIfUnacceptable(symAlg); InputStream decrypted = pkesk.getDataStream(decryptorFactory); + SessionKey sessionKey = new SessionKey(pkesk.getSessionKey(decryptorFactory)); + throwIfUnacceptable(sessionKey.getAlgorithm()); + MessageMetadata.EncryptedData encryptedData = new MessageMetadata.EncryptedData( SymmetricKeyAlgorithm.requireFromId(pkesk.getSymmetricAlgorithm(decryptorFactory))); + encryptedData.sessionKey = sessionKey; nestedInputStream = new OpenPgpMessageInputStream(buffer(decrypted), options, encryptedData, policy); return true; @@ -348,8 +353,12 @@ public class OpenPgpMessageInputStream extends DecryptionStream { try { InputStream decrypted = pkesk.getDataStream(decryptorFactory); + SessionKey sessionKey = new SessionKey(pkesk.getSessionKey(decryptorFactory)); + throwIfUnacceptable(sessionKey.getAlgorithm()); + MessageMetadata.EncryptedData encryptedData = new MessageMetadata.EncryptedData( SymmetricKeyAlgorithm.requireFromId(pkesk.getSymmetricAlgorithm(decryptorFactory))); + encryptedData.sessionKey = sessionKey; nestedInputStream = new OpenPgpMessageInputStream(buffer(decrypted), options, encryptedData, policy); return true; } catch (PGPException e) { @@ -427,7 +436,7 @@ public class OpenPgpMessageInputStream extends DecryptionStream { } catch (PGPException e) { throw new RuntimeException(e); } - signatures.finish(metadata); + signatures.finish(metadata, policy); } return r; } @@ -455,7 +464,7 @@ public class OpenPgpMessageInputStream extends DecryptionStream { } catch (PGPException e) { throw new RuntimeException(e); } - signatures.finish(metadata); + signatures.finish(metadata, policy); } return r; } @@ -588,7 +597,7 @@ public class OpenPgpMessageInputStream extends DecryptionStream { } } - void addCorrespondingOnePassSignature(PGPSignature signature, MessageMetadata.Layer layer) { + void addCorrespondingOnePassSignature(PGPSignature signature, MessageMetadata.Layer layer, Policy policy) { for (int i = onePassSignatures.size() - 1; i >= 0; i--) { OnePassSignature onePassSignature = onePassSignatures.get(i); if (onePassSignature.opSignature.getKeyID() != signature.getKeyID()) { @@ -598,19 +607,32 @@ public class OpenPgpMessageInputStream extends DecryptionStream { continue; } - boolean verified = onePassSignature.verify(signature); + boolean correct = onePassSignature.verify(signature); SignatureVerification verification = new SignatureVerification(signature, new SubkeyIdentifier(onePassSignature.certificate, onePassSignature.keyId)); - if (verified) { - layer.addVerifiedSignature(verification); + if (correct) { + PGPPublicKey signingKey = onePassSignature.certificate.getPublicKey(onePassSignature.keyId); + try { + checkSignatureValidity(signature, signingKey, policy); + layer.addVerifiedOnePassSignature(verification); + } catch (SignatureValidationException e) { + layer.addRejectedOnePassSignature(new SignatureVerification.Failure(verification, e)); + } } else { - layer.addFailedSignature(new SignatureVerification.Failure(verification, - new SignatureValidationException("Incorrect Signature."))); + layer.addRejectedOnePassSignature(new SignatureVerification.Failure(verification, + new SignatureValidationException("Bad Signature."))); } break; } } + boolean checkSignatureValidity(PGPSignature signature, PGPPublicKey signingKey, Policy policy) throws SignatureValidationException { + SignatureValidator.wasPossiblyMadeByKey(signingKey).verify(signature); + SignatureValidator.signatureStructureIsAcceptable(signingKey, policy).verify(signature); + SignatureValidator.signatureIsEffective().verify(signature); + return true; + } + void enterNesting() { opsUpdateStack.push(literalOPS); literalOPS = new ArrayList<>(); @@ -699,27 +721,39 @@ public class OpenPgpMessageInputStream extends DecryptionStream { } } - public void finish(MessageMetadata.Layer layer) { + public void finish(MessageMetadata.Layer layer, Policy policy) { for (DetachedOrPrependedSignature detached : detachedSignatures) { - boolean verified = detached.verify(); + boolean correct = detached.verify(); SignatureVerification verification = new SignatureVerification( detached.signature, new SubkeyIdentifier(detached.certificate, detached.keyId)); - if (verified) { - layer.addVerifiedSignature(verification); + if (correct) { + try { + PGPPublicKey signingKey = detached.certificate.getPublicKey(detached.keyId); + checkSignatureValidity(detached.signature, signingKey, policy); + layer.addVerifiedDetachedSignature(verification); + } catch (SignatureValidationException e) { + layer.addRejectedDetachedSignature(new SignatureVerification.Failure(verification, e)); + } } else { - layer.addFailedSignature(new SignatureVerification.Failure( - verification, new SignatureValidationException("Incorrect Signature."))); + layer.addRejectedDetachedSignature(new SignatureVerification.Failure( + verification, new SignatureValidationException("Incorrect Signature."))); } } for (DetachedOrPrependedSignature prepended : prependedSignatures) { - boolean verified = prepended.verify(); + boolean correct = prepended.verify(); SignatureVerification verification = new SignatureVerification( prepended.signature, new SubkeyIdentifier(prepended.certificate, prepended.keyId)); - if (verified) { - layer.addVerifiedSignature(verification); + if (correct) { + try { + PGPPublicKey signingKey = prepended.certificate.getPublicKey(prepended.keyId); + checkSignatureValidity(prepended.signature, signingKey, policy); + layer.addVerifiedPrependedSignature(verification); + } catch (SignatureValidationException e) { + layer.addRejectedPrependedSignature(new SignatureVerification.Failure(verification, e)); + } } else { - layer.addFailedSignature(new SignatureVerification.Failure( + layer.addRejectedPrependedSignature(new SignatureVerification.Failure( verification, new SignatureValidationException("Incorrect Signature."))); } } @@ -859,46 +893,18 @@ public class OpenPgpMessageInputStream extends DecryptionStream { resultBuilder.setFileEncoding(m.getFormat()); resultBuilder.setSessionKey(m.getSessionKey()); - for (Signatures.OnePassSignature ops : signatures.onePassSignatures) { - if (!ops.finished) { - continue; - } - - SubkeyIdentifier identifier = new SubkeyIdentifier(ops.certificate, ops.keyId); - SignatureVerification verification = new SignatureVerification(ops.signature, identifier); - if (ops.valid) { - resultBuilder.addVerifiedInbandSignature(verification); - } else { - resultBuilder.addInvalidInbandSignature(verification, new SignatureValidationException("Incorrect signature.")); - } + for (SignatureVerification accepted : m.getVerifiedDetachedSignatures()) { + resultBuilder.addVerifiedDetachedSignature(accepted); + } + for (SignatureVerification.Failure rejected : m.getRejectedDetachedSignatures()) { + resultBuilder.addInvalidDetachedSignature(rejected.getSignatureVerification(), rejected.getValidationException()); } - for (Signatures.DetachedOrPrependedSignature prep : signatures.prependedSignatures) { - if (!prep.finished) { - continue; - } - - SubkeyIdentifier identifier = new SubkeyIdentifier(prep.certificate, prep.keyId); - SignatureVerification verification = new SignatureVerification(prep.signature, identifier); - if (prep.valid) { - resultBuilder.addVerifiedInbandSignature(verification); - } else { - resultBuilder.addInvalidInbandSignature(verification, new SignatureValidationException("Incorrect signature.")); - } + for (SignatureVerification accepted : m.getVerifiedInlineSignatures()) { + resultBuilder.addVerifiedInbandSignature(accepted); } - - for (Signatures.DetachedOrPrependedSignature det : signatures.detachedSignatures) { - if (!det.finished) { - continue; - } - - SubkeyIdentifier identifier = new SubkeyIdentifier(det.certificate, det.keyId); - SignatureVerification verification = new SignatureVerification(det.signature, identifier); - if (det.valid) { - resultBuilder.addVerifiedDetachedSignature(verification); - } else { - resultBuilder.addInvalidDetachedSignature(verification, new SignatureValidationException("Incorrect signature.")); - } + for (SignatureVerification.Failure rejected : m.getRejectedInlineSignatures()) { + resultBuilder.addInvalidInbandSignature(rejected.getSignatureVerification(), rejected.getValidationException()); } return resultBuilder.build(); diff --git a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStreamTest.java b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStreamTest.java index 87ae27f8..a8969047 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStreamTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStreamTest.java @@ -332,8 +332,8 @@ public class OpenPgpMessageInputStreamTest { assertEquals("", metadata.getFilename()); JUtils.assertDateEquals(new Date(0L), metadata.getModificationDate()); assertEquals(StreamEncoding.BINARY, metadata.getFormat()); - assertTrue(metadata.getVerifiedSignatures().isEmpty()); - assertTrue(metadata.getRejectedSignatures().isEmpty()); + assertTrue(metadata.getVerifiedInlineSignatures().isEmpty()); + assertTrue(metadata.getRejectedInlineSignatures().isEmpty()); } @ParameterizedTest(name = "Process LIT LIT using {0}") @@ -352,8 +352,8 @@ public class OpenPgpMessageInputStreamTest { assertEquals(PLAINTEXT, plain); MessageMetadata metadata = result.getB(); assertEquals(CompressionAlgorithm.ZIP, metadata.getCompressionAlgorithm()); - assertTrue(metadata.getVerifiedSignatures().isEmpty()); - assertTrue(metadata.getRejectedSignatures().isEmpty()); + assertTrue(metadata.getVerifiedInlineSignatures().isEmpty()); + assertTrue(metadata.getRejectedInlineSignatures().isEmpty()); } @ParameterizedTest(name = "Process COMP using {0}") @@ -377,8 +377,8 @@ public class OpenPgpMessageInputStreamTest { assertEquals(CompressionAlgorithm.BZIP2, compressionAlgorithms.next()); assertFalse(compressionAlgorithms.hasNext()); assertNull(metadata.getEncryptionAlgorithm()); - assertTrue(metadata.getVerifiedSignatures().isEmpty()); - assertTrue(metadata.getRejectedSignatures().isEmpty()); + assertTrue(metadata.getVerifiedInlineSignatures().isEmpty()); + assertTrue(metadata.getRejectedInlineSignatures().isEmpty()); } @ParameterizedTest(name = "Process SIG COMP(LIT) using {0}") @@ -395,8 +395,8 @@ public class OpenPgpMessageInputStreamTest { MessageMetadata metadata = result.getB(); assertEquals(CompressionAlgorithm.ZIP, metadata.getCompressionAlgorithm()); assertNull(metadata.getEncryptionAlgorithm()); - assertFalse(metadata.getVerifiedSignatures().isEmpty()); - assertTrue(metadata.getRejectedSignatures().isEmpty()); + assertFalse(metadata.getVerifiedInlineSignatures().isEmpty()); + assertTrue(metadata.getRejectedInlineSignatures().isEmpty()); } @ParameterizedTest(name = "Process SENC(LIT) using {0}") @@ -410,8 +410,8 @@ public class OpenPgpMessageInputStreamTest { MessageMetadata metadata = result.getB(); assertNull(metadata.getCompressionAlgorithm()); assertEquals(SymmetricKeyAlgorithm.AES_256, metadata.getEncryptionAlgorithm()); - assertTrue(metadata.getVerifiedSignatures().isEmpty()); - assertTrue(metadata.getRejectedSignatures().isEmpty()); + assertTrue(metadata.getVerifiedInlineSignatures().isEmpty()); + assertTrue(metadata.getRejectedInlineSignatures().isEmpty()); } @ParameterizedTest(name = "Process PENC(COMP(LIT)) using {0}") @@ -426,8 +426,8 @@ public class OpenPgpMessageInputStreamTest { MessageMetadata metadata = result.getB(); assertEquals(CompressionAlgorithm.ZLIB, metadata.getCompressionAlgorithm()); assertEquals(SymmetricKeyAlgorithm.AES_256, metadata.getEncryptionAlgorithm()); - assertTrue(metadata.getVerifiedSignatures().isEmpty()); - assertTrue(metadata.getRejectedSignatures().isEmpty()); + assertTrue(metadata.getVerifiedInlineSignatures().isEmpty()); + assertTrue(metadata.getRejectedInlineSignatures().isEmpty()); } @ParameterizedTest(name = "Process OPS LIT SIG using {0}") @@ -442,8 +442,8 @@ public class OpenPgpMessageInputStreamTest { MessageMetadata metadata = result.getB(); assertNull(metadata.getEncryptionAlgorithm()); assertNull(metadata.getCompressionAlgorithm()); - assertFalse(metadata.getVerifiedSignatures().isEmpty()); - assertTrue(metadata.getRejectedSignatures().isEmpty()); + assertFalse(metadata.getVerifiedInlineSignatures().isEmpty()); + assertTrue(metadata.getRejectedInlineSignatures().isEmpty()); } String BOB_KEY = "-----BEGIN PGP PRIVATE KEY BLOCK-----\n" + @@ -579,8 +579,8 @@ public class OpenPgpMessageInputStreamTest { MessageMetadata metadata = result.getB(); assertEquals(SymmetricKeyAlgorithm.AES_256, metadata.getEncryptionAlgorithm()); assertNull(metadata.getCompressionAlgorithm()); - assertFalse(metadata.getVerifiedSignatures().isEmpty()); - assertTrue(metadata.getRejectedSignatures().isEmpty()); + assertFalse(metadata.getVerifiedInlineSignatures().isEmpty()); + assertTrue(metadata.getRejectedInlineSignatures().isEmpty()); } @ParameterizedTest(name = "Process PENC(OPS OPS OPS LIT SIG SIG SIG) using {0}") @@ -644,8 +644,8 @@ public class OpenPgpMessageInputStreamTest { MessageMetadata metadata = result.getB(); assertEquals(SymmetricKeyAlgorithm.AES_256, metadata.getEncryptionAlgorithm()); assertNull(metadata.getCompressionAlgorithm()); - assertFalse(metadata.getVerifiedSignatures().isEmpty()); - assertTrue(metadata.getRejectedSignatures().isEmpty()); + assertFalse(metadata.getVerifiedInlineSignatures().isEmpty()); + assertTrue(metadata.getRejectedInlineSignatures().isEmpty()); } private static Tuple processReadBuffered(String armoredMessage, ConsumerOptions options) From fbcde13df37bfd471d191b817ff5e794c39e9d23 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Sun, 16 Oct 2022 19:12:17 +0200 Subject: [PATCH 042/763] Reinstate integrity-protection and fix tests Integrity Protection is now checked when reading from the stream, not only when closing. --- .../OpenPgpMessageInputStream.java | 17 +++++++++---- .../TeeBCPGInputStream.java | 20 ++++++++++++++-- .../ModificationDetectionTests.java | 24 ++++++++++++------- 3 files changed, 46 insertions(+), 15 deletions(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java index a4b05bbe..54172e7f 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java @@ -272,13 +272,15 @@ public class OpenPgpMessageInputStream extends DecryptionStream { PGPPBEEncryptedData skesk = (PGPPBEEncryptedData) esk; InputStream decrypted = skesk.getDataStream(decryptorFactory); encryptedData.sessionKey = sessionKey; - nestedInputStream = new OpenPgpMessageInputStream(buffer(decrypted), options, encryptedData, policy); + IntegrityProtectedInputStream integrityProtected = new IntegrityProtectedInputStream(decrypted, skesk, options); + nestedInputStream = new OpenPgpMessageInputStream(buffer(integrityProtected), options, encryptedData, policy); return true; } else if (esk instanceof PGPPublicKeyEncryptedData) { PGPPublicKeyEncryptedData pkesk = (PGPPublicKeyEncryptedData) esk; InputStream decrypted = pkesk.getDataStream(decryptorFactory); encryptedData.sessionKey = sessionKey; - nestedInputStream = new OpenPgpMessageInputStream(buffer(decrypted), options, encryptedData, policy); + IntegrityProtectedInputStream integrityProtected = new IntegrityProtectedInputStream(decrypted, pkesk, options); + nestedInputStream = new OpenPgpMessageInputStream(buffer(integrityProtected), options, encryptedData, policy); return true; } else { throw new RuntimeException("Unknown ESK class type: " + esk.getClass().getName()); @@ -302,7 +304,8 @@ public class OpenPgpMessageInputStream extends DecryptionStream { throwIfUnacceptable(sessionKey.getAlgorithm()); MessageMetadata.EncryptedData encryptedData = new MessageMetadata.EncryptedData(sessionKey.getAlgorithm()); encryptedData.sessionKey = sessionKey; - nestedInputStream = new OpenPgpMessageInputStream(buffer(decrypted), options, encryptedData, policy); + IntegrityProtectedInputStream integrityProtected = new IntegrityProtectedInputStream(decrypted, skesk, options); + nestedInputStream = new OpenPgpMessageInputStream(buffer(integrityProtected), options, encryptedData, policy); return true; } catch (UnacceptableAlgorithmException e) { throw e; @@ -334,7 +337,8 @@ public class OpenPgpMessageInputStream extends DecryptionStream { SymmetricKeyAlgorithm.requireFromId(pkesk.getSymmetricAlgorithm(decryptorFactory))); encryptedData.sessionKey = sessionKey; - nestedInputStream = new OpenPgpMessageInputStream(buffer(decrypted), options, encryptedData, policy); + IntegrityProtectedInputStream integrityProtected = new IntegrityProtectedInputStream(decrypted, pkesk, options); + nestedInputStream = new OpenPgpMessageInputStream(buffer(integrityProtected), options, encryptedData, policy); return true; } catch (UnacceptableAlgorithmException e) { throw e; @@ -359,7 +363,9 @@ public class OpenPgpMessageInputStream extends DecryptionStream { MessageMetadata.EncryptedData encryptedData = new MessageMetadata.EncryptedData( SymmetricKeyAlgorithm.requireFromId(pkesk.getSymmetricAlgorithm(decryptorFactory))); encryptedData.sessionKey = sessionKey; - nestedInputStream = new OpenPgpMessageInputStream(buffer(decrypted), options, encryptedData, policy); + + IntegrityProtectedInputStream integrityProtected = new IntegrityProtectedInputStream(decrypted, pkesk, options); + nestedInputStream = new OpenPgpMessageInputStream(buffer(integrityProtected), options, encryptedData, policy); return true; } catch (PGPException e) { // hm :/ @@ -491,6 +497,7 @@ public class OpenPgpMessageInputStream extends DecryptionStream { automaton.next(InputAlphabet.EndOfSequence); automaton.assertValid(); + packetInputStream.close(); closed = true; } diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/TeeBCPGInputStream.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/TeeBCPGInputStream.java index f80793f0..2efcfc43 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/TeeBCPGInputStream.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/TeeBCPGInputStream.java @@ -96,6 +96,12 @@ public class TeeBCPGInputStream { return markerPacket; } + + public void close() throws IOException { + this.packetInputStream.close(); + this.delayedTee.close(); + } + public static class DelayedTeeInputStreamInputStream extends InputStream { private int last = -1; @@ -112,8 +118,12 @@ public class TeeBCPGInputStream { if (last != -1) { outputStream.write(last); } - last = inputStream.read(); - return last; + try { + last = inputStream.read(); + return last; + } catch (IOException e) { + return -1; + } } /** @@ -127,5 +137,11 @@ public class TeeBCPGInputStream { } last = -1; } + + @Override + public void close() throws IOException { + inputStream.close(); + outputStream.close(); + } } } diff --git a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/ModificationDetectionTests.java b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/ModificationDetectionTests.java index 14a041ff..9ecaa38a 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/ModificationDetectionTests.java +++ b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/ModificationDetectionTests.java @@ -238,8 +238,10 @@ public class ModificationDetectionTests { ); ByteArrayOutputStream out = new ByteArrayOutputStream(); - Streams.pipeAll(decryptionStream, out); - assertThrows(ModificationDetectionException.class, decryptionStream::close); + assertThrows(ModificationDetectionException.class, () -> { + Streams.pipeAll(decryptionStream, out); + decryptionStream.close(); + }); } @TestTemplate @@ -269,8 +271,10 @@ public class ModificationDetectionTests { ); ByteArrayOutputStream out = new ByteArrayOutputStream(); - Streams.pipeAll(decryptionStream, out); - assertThrows(ModificationDetectionException.class, decryptionStream::close); + assertThrows(ModificationDetectionException.class, () -> { + Streams.pipeAll(decryptionStream, out); + decryptionStream.close(); + }); } @TestTemplate @@ -313,8 +317,10 @@ public class ModificationDetectionTests { ); ByteArrayOutputStream out = new ByteArrayOutputStream(); - Streams.pipeAll(decryptionStream, out); - assertThrows(ModificationDetectionException.class, decryptionStream::close); + assertThrows(ModificationDetectionException.class, () -> { + Streams.pipeAll(decryptionStream, out); + decryptionStream.close(); + }); } @TestTemplate @@ -344,8 +350,10 @@ public class ModificationDetectionTests { ); ByteArrayOutputStream out = new ByteArrayOutputStream(); - Streams.pipeAll(decryptionStream, out); - assertThrows(ModificationDetectionException.class, decryptionStream::close); + assertThrows(ModificationDetectionException.class, () -> { + Streams.pipeAll(decryptionStream, out); + decryptionStream.close(); + }); } @TestTemplate From 6fd705b1dc6de71ea78939d6a1edeae03dfb39cd Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 17 Oct 2022 00:53:50 +0200 Subject: [PATCH 043/763] Fix checkstyle issues --- .../decryption_verification/DecryptionStreamFactory.java | 1 - .../decryption_verification/OpenPgpMessageInputStream.java | 5 ++--- .../InvestigateMultiSEIPMessageHandlingTest.java | 2 -- .../PreventDecryptionUsingNonEncryptionKeyTest.java | 2 -- 4 files changed, 2 insertions(+), 8 deletions(-) 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 680f33e4..5e6b2aec 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 @@ -40,7 +40,6 @@ import org.bouncycastle.openpgp.operator.PBEDataDecryptorFactory; import org.bouncycastle.openpgp.operator.PGPContentVerifierBuilderProvider; import org.bouncycastle.openpgp.operator.PublicKeyDataDecryptorFactory; import org.bouncycastle.openpgp.operator.SessionKeyDataDecryptorFactory; -import org.graalvm.compiler.lir.amd64.AMD64BinaryConsumer; import org.pgpainless.PGPainless; import org.pgpainless.algorithm.CompressionAlgorithm; import org.pgpainless.algorithm.EncryptionPurpose; diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java index 54172e7f..37ccff37 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java @@ -29,7 +29,6 @@ import org.bouncycastle.openpgp.PGPPublicKeyEncryptedData; import org.bouncycastle.openpgp.PGPPublicKeyRing; import org.bouncycastle.openpgp.PGPSecretKey; import org.bouncycastle.openpgp.PGPSecretKeyRing; -import org.bouncycastle.openpgp.PGPSessionKey; import org.bouncycastle.openpgp.PGPSignature; import org.bouncycastle.openpgp.operator.PBEDataDecryptorFactory; import org.bouncycastle.openpgp.operator.PGPContentVerifierBuilderProvider; @@ -795,7 +794,7 @@ public class OpenPgpMessageInputStream extends DecryptionStream { boolean finished; boolean valid; - public DetachedOrPrependedSignature(PGPSignature signature, PGPPublicKeyRing certificate, long keyId) { + DetachedOrPrependedSignature(PGPSignature signature, PGPPublicKeyRing certificate, long keyId) { this.signature = signature; this.certificate = certificate; this.keyId = keyId; @@ -844,7 +843,7 @@ public class OpenPgpMessageInputStream extends DecryptionStream { boolean finished; boolean valid; - public OnePassSignature(PGPOnePassSignature signature, PGPPublicKeyRing certificate, long keyId) { + OnePassSignature(PGPOnePassSignature signature, PGPPublicKeyRing certificate, long keyId) { this.opSignature = signature; this.certificate = certificate; this.keyId = keyId; diff --git a/pgpainless-core/src/test/java/investigations/InvestigateMultiSEIPMessageHandlingTest.java b/pgpainless-core/src/test/java/investigations/InvestigateMultiSEIPMessageHandlingTest.java index d31a0e0f..a0ea747a 100644 --- a/pgpainless-core/src/test/java/investigations/InvestigateMultiSEIPMessageHandlingTest.java +++ b/pgpainless-core/src/test/java/investigations/InvestigateMultiSEIPMessageHandlingTest.java @@ -4,8 +4,6 @@ package investigations; -import static org.junit.jupiter.api.Assertions.assertArrayEquals; -import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; import java.io.ByteArrayInputStream; diff --git a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/PreventDecryptionUsingNonEncryptionKeyTest.java b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/PreventDecryptionUsingNonEncryptionKeyTest.java index 7474301b..ba80c69d 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/PreventDecryptionUsingNonEncryptionKeyTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/PreventDecryptionUsingNonEncryptionKeyTest.java @@ -8,8 +8,6 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.EOFException; import java.io.IOException; import java.nio.charset.StandardCharsets; From 7097d449169283751471631f5969d50ee2e41e60 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 17 Oct 2022 02:47:11 +0200 Subject: [PATCH 044/763] Fix NPEs and expose decryption keys --- .../MessageMetadata.java | 20 +++++++++++++ .../OpenPgpMessageInputStream.java | 30 ++++++++++++------- 2 files changed, 40 insertions(+), 10 deletions(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/MessageMetadata.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/MessageMetadata.java index 2cd2a6f2..1be47112 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/MessageMetadata.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/MessageMetadata.java @@ -7,6 +7,7 @@ package org.pgpainless.decryption_verification; import org.pgpainless.algorithm.CompressionAlgorithm; import org.pgpainless.algorithm.StreamEncoding; import org.pgpainless.algorithm.SymmetricKeyAlgorithm; +import org.pgpainless.key.SubkeyIdentifier; import org.pgpainless.util.SessionKey; import javax.annotation.Nonnull; @@ -182,6 +183,24 @@ public class MessageMetadata { return (LiteralData) nested; } + public SubkeyIdentifier getDecryptionKey() { + Iterator iterator = new LayerIterator(message) { + @Override + public boolean matches(Nested layer) { + return layer instanceof EncryptedData; + } + + @Override + public SubkeyIdentifier getProperty(Layer last) { + return ((EncryptedData) last).decryptionKey; + } + }; + if (iterator.hasNext()) { + return iterator.next(); + } + return null; + } + public abstract static class Layer { protected final List verifiedDetachedSignatures = new ArrayList<>(); protected final List rejectedDetachedSignatures = new ArrayList<>(); @@ -309,6 +328,7 @@ public class MessageMetadata { public static class EncryptedData extends Layer implements Nested { protected final SymmetricKeyAlgorithm algorithm; + protected SubkeyIdentifier decryptionKey; protected SessionKey sessionKey; protected List recipients; diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java index 37ccff37..944ce6e4 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java @@ -334,6 +334,7 @@ public class OpenPgpMessageInputStream extends DecryptionStream { MessageMetadata.EncryptedData encryptedData = new MessageMetadata.EncryptedData( SymmetricKeyAlgorithm.requireFromId(pkesk.getSymmetricAlgorithm(decryptorFactory))); + encryptedData.decryptionKey = new SubkeyIdentifier(decryptionKeys, decryptionKey.getKeyID()); encryptedData.sessionKey = sessionKey; IntegrityProtectedInputStream integrityProtected = new IntegrityProtectedInputStream(decrypted, pkesk, options); @@ -361,6 +362,7 @@ public class OpenPgpMessageInputStream extends DecryptionStream { MessageMetadata.EncryptedData encryptedData = new MessageMetadata.EncryptedData( SymmetricKeyAlgorithm.requireFromId(pkesk.getSymmetricAlgorithm(decryptorFactory))); + encryptedData.decryptionKey = new SubkeyIdentifier(decryptionKeyCandidate.getA(), privateKey.getKeyID()); encryptedData.sessionKey = sessionKey; IntegrityProtectedInputStream integrityProtected = new IntegrityProtectedInputStream(decrypted, pkesk, options); @@ -560,8 +562,6 @@ public class OpenPgpMessageInputStream extends DecryptionStream { final List correspondingSignatures; boolean isLiteral = true; - final List verified = new ArrayList<>(); - private Signatures(ConsumerOptions options) { this.options = options; this.detachedSignatures = new ArrayList<>(); @@ -580,24 +580,33 @@ public class OpenPgpMessageInputStream extends DecryptionStream { void addDetachedSignature(PGPSignature signature) { long keyId = SignatureUtils.determineIssuerKeyId(signature); PGPPublicKeyRing certificate = findCertificate(keyId); - initialize(signature, certificate, keyId); - this.detachedSignatures.add(new DetachedOrPrependedSignature(signature, certificate, keyId)); + + if (certificate != null) { + initialize(signature, certificate, keyId); + this.detachedSignatures.add(new DetachedOrPrependedSignature(signature, certificate, keyId)); + } } void addPrependedSignature(PGPSignature signature) { long keyId = SignatureUtils.determineIssuerKeyId(signature); PGPPublicKeyRing certificate = findCertificate(keyId); - initialize(signature, certificate, keyId); - this.prependedSignatures.add(new DetachedOrPrependedSignature(signature, certificate, keyId)); + + if (certificate != null) { + initialize(signature, certificate, keyId); + this.prependedSignatures.add(new DetachedOrPrependedSignature(signature, certificate, keyId)); + } } void addOnePassSignature(PGPOnePassSignature signature) { PGPPublicKeyRing certificate = findCertificate(signature.getKeyID()); - OnePassSignature ops = new OnePassSignature(signature, certificate, signature.getKeyID()); - ops.init(certificate); - onePassSignatures.add(ops); - literalOPS.add(ops); + if (certificate != null) { + OnePassSignature ops = new OnePassSignature(signature, certificate, signature.getKeyID()); + ops.init(certificate); + onePassSignatures.add(ops); + + literalOPS.add(ops); + } if (signature.isContaining()) { enterNesting(); } @@ -898,6 +907,7 @@ public class OpenPgpMessageInputStream extends DecryptionStream { resultBuilder.setFileName(m.getFilename()); resultBuilder.setFileEncoding(m.getFormat()); resultBuilder.setSessionKey(m.getSessionKey()); + resultBuilder.setDecryptionKey(m.getDecryptionKey()); for (SignatureVerification accepted : m.getVerifiedDetachedSignatures()) { resultBuilder.addVerifiedDetachedSignature(accepted); From dfbb01d61cc063ff849d64aa7f21441a7c9facd9 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 18 Oct 2022 15:55:47 +0200 Subject: [PATCH 045/763] Enfore max recursion depth and fix CRC test --- .../MessageMetadata.java | 19 ++++++++- .../OpenPgpMessageInputStream.java | 39 ++++++++++++++++--- .../org/bouncycastle/AsciiArmorCRCTests.java | 20 +++++----- .../MessageMetadataTest.java | 6 +-- .../RecursionDepthTest.java | 4 +- 5 files changed, 66 insertions(+), 22 deletions(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/MessageMetadata.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/MessageMetadata.java index 1be47112..59f8052a 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/MessageMetadata.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/MessageMetadata.java @@ -7,6 +7,7 @@ package org.pgpainless.decryption_verification; import org.pgpainless.algorithm.CompressionAlgorithm; import org.pgpainless.algorithm.StreamEncoding; import org.pgpainless.algorithm.SymmetricKeyAlgorithm; +import org.pgpainless.exception.MalformedOpenPgpMessageException; import org.pgpainless.key.SubkeyIdentifier; import org.pgpainless.util.SessionKey; @@ -202,6 +203,8 @@ public class MessageMetadata { } public abstract static class Layer { + public static final int MAX_LAYER_DEPTH = 16; + protected final int depth; protected final List verifiedDetachedSignatures = new ArrayList<>(); protected final List rejectedDetachedSignatures = new ArrayList<>(); protected final List verifiedOnePassSignatures = new ArrayList<>(); @@ -210,6 +213,13 @@ public class MessageMetadata { protected final List rejectedPrependedSignatures = new ArrayList<>(); protected Nested child; + public Layer(int depth) { + this.depth = depth; + if (depth > MAX_LAYER_DEPTH) { + throw new MalformedOpenPgpMessageException("Maximum nesting depth exceeded."); + } + } + public Nested getChild() { return child; } @@ -274,6 +284,9 @@ public class MessageMetadata { public static class Message extends Layer { + public Message() { + super(0); + } } public static class LiteralData implements Nested { @@ -312,7 +325,8 @@ public class MessageMetadata { public static class CompressedData extends Layer implements Nested { protected final CompressionAlgorithm algorithm; - public CompressedData(CompressionAlgorithm zip) { + public CompressedData(CompressionAlgorithm zip, int depth) { + super(depth); this.algorithm = zip; } @@ -332,7 +346,8 @@ public class MessageMetadata { protected SessionKey sessionKey; protected List recipients; - public EncryptedData(SymmetricKeyAlgorithm algorithm) { + public EncryptedData(SymmetricKeyAlgorithm algorithm, int depth) { + super(depth); this.algorithm = algorithm; } diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java index 944ce6e4..f5ed556a 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java @@ -15,6 +15,7 @@ import java.util.List; import java.util.Stack; import javax.annotation.Nonnull; +import org.bouncycastle.bcpg.ArmoredInputStream; import org.bouncycastle.bcpg.BCPGInputStream; import org.bouncycastle.openpgp.PGPCompressedData; import org.bouncycastle.openpgp.PGPEncryptedData; @@ -56,6 +57,7 @@ import org.pgpainless.key.protection.UnlockSecretKey; import org.pgpainless.policy.Policy; import org.pgpainless.signature.SignatureUtils; import org.pgpainless.signature.consumer.SignatureValidator; +import org.pgpainless.util.ArmoredInputStreamFactory; import org.pgpainless.util.Passphrase; import org.pgpainless.util.SessionKey; import org.pgpainless.util.Tuple; @@ -91,7 +93,7 @@ public class OpenPgpMessageInputStream extends DecryptionStream { @Nonnull ConsumerOptions options, @Nonnull Policy policy) throws PGPException, IOException { - this(inputStream, options, new MessageMetadata.Message(), policy); + this(prepareInputStream(inputStream, options), options, new MessageMetadata.Message(), policy); } protected OpenPgpMessageInputStream(@Nonnull InputStream inputStream, @@ -118,6 +120,26 @@ public class OpenPgpMessageInputStream extends DecryptionStream { consumePackets(); } + private static InputStream prepareInputStream(InputStream inputStream, ConsumerOptions options) throws IOException { + OpenPgpInputStream openPgpIn = new OpenPgpInputStream(inputStream); + openPgpIn.reset(); + + if (openPgpIn.isBinaryOpenPgp()) { + return openPgpIn; + } + + if (openPgpIn.isAsciiArmored()) { + ArmoredInputStream armorIn = ArmoredInputStreamFactory.get(openPgpIn); + if (armorIn.isClearText()) { + return armorIn; + } else { + return armorIn; + } + } else { + return openPgpIn; + } + } + /** * Consume OpenPGP packets from the current {@link BCPGInputStream}. * Once an OpenPGP packet with nested data (Literal Data, Compressed Data, Encrypted Data) is reached, @@ -219,7 +241,8 @@ public class OpenPgpMessageInputStream extends DecryptionStream { signatures.enterNesting(); PGPCompressedData compressedData = packetInputStream.readCompressedData(); MessageMetadata.CompressedData compressionLayer = new MessageMetadata.CompressedData( - CompressionAlgorithm.fromId(compressedData.getAlgorithm())); + CompressionAlgorithm.fromId(compressedData.getAlgorithm()), + metadata.depth + 1); InputStream decompressed = compressedData.getDataStream(); nestedInputStream = new OpenPgpMessageInputStream(buffer(decompressed), options, compressionLayer, policy); } @@ -262,7 +285,8 @@ public class OpenPgpMessageInputStream extends DecryptionStream { SessionKeyDataDecryptorFactory decryptorFactory = ImplementationFactory.getInstance() .getSessionKeyDataDecryptorFactory(sessionKey); - MessageMetadata.EncryptedData encryptedData = new MessageMetadata.EncryptedData(sessionKey.getAlgorithm()); + MessageMetadata.EncryptedData encryptedData = new MessageMetadata.EncryptedData( + sessionKey.getAlgorithm(), metadata.depth + 1); try { // TODO: Use BCs new API once shipped @@ -301,7 +325,8 @@ public class OpenPgpMessageInputStream extends DecryptionStream { InputStream decrypted = skesk.getDataStream(decryptorFactory); SessionKey sessionKey = new SessionKey(skesk.getSessionKey(decryptorFactory)); throwIfUnacceptable(sessionKey.getAlgorithm()); - MessageMetadata.EncryptedData encryptedData = new MessageMetadata.EncryptedData(sessionKey.getAlgorithm()); + MessageMetadata.EncryptedData encryptedData = new MessageMetadata.EncryptedData( + sessionKey.getAlgorithm(), metadata.depth + 1); encryptedData.sessionKey = sessionKey; IntegrityProtectedInputStream integrityProtected = new IntegrityProtectedInputStream(decrypted, skesk, options); nestedInputStream = new OpenPgpMessageInputStream(buffer(integrityProtected), options, encryptedData, policy); @@ -333,7 +358,8 @@ public class OpenPgpMessageInputStream extends DecryptionStream { throwIfUnacceptable(sessionKey.getAlgorithm()); MessageMetadata.EncryptedData encryptedData = new MessageMetadata.EncryptedData( - SymmetricKeyAlgorithm.requireFromId(pkesk.getSymmetricAlgorithm(decryptorFactory))); + SymmetricKeyAlgorithm.requireFromId(pkesk.getSymmetricAlgorithm(decryptorFactory)), + metadata.depth + 1); encryptedData.decryptionKey = new SubkeyIdentifier(decryptionKeys, decryptionKey.getKeyID()); encryptedData.sessionKey = sessionKey; @@ -361,7 +387,8 @@ public class OpenPgpMessageInputStream extends DecryptionStream { throwIfUnacceptable(sessionKey.getAlgorithm()); MessageMetadata.EncryptedData encryptedData = new MessageMetadata.EncryptedData( - SymmetricKeyAlgorithm.requireFromId(pkesk.getSymmetricAlgorithm(decryptorFactory))); + SymmetricKeyAlgorithm.requireFromId(pkesk.getSymmetricAlgorithm(decryptorFactory)), + metadata.depth + 1); encryptedData.decryptionKey = new SubkeyIdentifier(decryptionKeyCandidate.getA(), privateKey.getKeyID()); encryptedData.sessionKey = sessionKey; diff --git a/pgpainless-core/src/test/java/org/bouncycastle/AsciiArmorCRCTests.java b/pgpainless-core/src/test/java/org/bouncycastle/AsciiArmorCRCTests.java index c3af0bd6..f9bd7a61 100644 --- a/pgpainless-core/src/test/java/org/bouncycastle/AsciiArmorCRCTests.java +++ b/pgpainless-core/src/test/java/org/bouncycastle/AsciiArmorCRCTests.java @@ -489,7 +489,7 @@ public class AsciiArmorCRCTests { Passphrase passphrase = Passphrase.fromPassword("flowcrypt compatibility tests"); @Test - public void testInvalidArmorCRCThrowsOnClose() throws PGPException, IOException { + public void testInvalidArmorCRCThrowsOnClose() throws IOException { String message = "-----BEGIN PGP MESSAGE-----\n" + "Version: FlowCrypt 5.0.4 Gmail Encryption flowcrypt.com\n" + "Comment: Seamlessly send, receive and search encrypted email\n" + @@ -542,14 +542,16 @@ public class AsciiArmorCRCTests { "-----END PGP MESSAGE-----\n"; PGPSecretKeyRing key = PGPainless.readKeyRing().secretKeyRing(ASCII_KEY); - DecryptionStream decryptionStream = PGPainless.decryptAndOrVerify() - .onInputStream(new ByteArrayInputStream(message.getBytes(StandardCharsets.UTF_8))) - .withOptions(new ConsumerOptions().addDecryptionKey( - key, SecretKeyRingProtector.unlockAnyKeyWith(passphrase) - )); + assertThrows(IOException.class, () -> { + DecryptionStream decryptionStream = PGPainless.decryptAndOrVerify() + .onInputStream(new ByteArrayInputStream(message.getBytes(StandardCharsets.UTF_8))) + .withOptions(new ConsumerOptions().addDecryptionKey( + key, SecretKeyRingProtector.unlockAnyKeyWith(passphrase) + )); - ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); - Streams.pipeAll(decryptionStream, outputStream); - assertThrows(IOException.class, decryptionStream::close); + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + Streams.pipeAll(decryptionStream, outputStream); + decryptionStream.close(); + }); } } diff --git a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/MessageMetadataTest.java b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/MessageMetadataTest.java index 9f887eb9..d87fc6bf 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/MessageMetadataTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/MessageMetadataTest.java @@ -28,9 +28,9 @@ public class MessageMetadataTest { // For the sake of testing though, this is okay. MessageMetadata.Message message = new MessageMetadata.Message(); - MessageMetadata.CompressedData compressedData = new MessageMetadata.CompressedData(CompressionAlgorithm.ZIP); - MessageMetadata.EncryptedData encryptedData = new MessageMetadata.EncryptedData(SymmetricKeyAlgorithm.AES_128); - MessageMetadata.EncryptedData encryptedData1 = new MessageMetadata.EncryptedData(SymmetricKeyAlgorithm.AES_256); + MessageMetadata.CompressedData compressedData = new MessageMetadata.CompressedData(CompressionAlgorithm.ZIP, message.depth + 1); + MessageMetadata.EncryptedData encryptedData = new MessageMetadata.EncryptedData(SymmetricKeyAlgorithm.AES_128, compressedData.depth + 1); + MessageMetadata.EncryptedData encryptedData1 = new MessageMetadata.EncryptedData(SymmetricKeyAlgorithm.AES_256, encryptedData.depth + 1); MessageMetadata.LiteralData literalData = new MessageMetadata.LiteralData(); message.setChild(compressedData); diff --git a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/RecursionDepthTest.java b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/RecursionDepthTest.java index b253cf7f..52c5a906 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/RecursionDepthTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/RecursionDepthTest.java @@ -11,12 +11,12 @@ import java.io.ByteArrayOutputStream; import java.io.IOException; import java.nio.charset.StandardCharsets; -import org.bouncycastle.openpgp.PGPException; import org.bouncycastle.openpgp.PGPSecretKeyRing; import org.bouncycastle.util.io.Streams; import org.junit.jupiter.api.TestTemplate; import org.junit.jupiter.api.extension.ExtendWith; import org.pgpainless.PGPainless; +import org.pgpainless.exception.MalformedOpenPgpMessageException; import org.pgpainless.util.TestAllImplementations; public class RecursionDepthTest { @@ -143,7 +143,7 @@ public class RecursionDepthTest { "-----END PGP ARMORED FILE-----\n"; - assertThrows(PGPException.class, () -> { + assertThrows(MalformedOpenPgpMessageException.class, () -> { DecryptionStream decryptionStream = PGPainless.decryptAndOrVerify() .onInputStream(new ByteArrayInputStream(msg.getBytes(StandardCharsets.UTF_8))) .withOptions(new ConsumerOptions().addDecryptionKey(secretKey)); From d3f07a2250d13eab224efc6d08b1c4c5ebebeac9 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 20 Oct 2022 14:05:21 +0200 Subject: [PATCH 046/763] Reuse *SignatureCheck class --- .../DecryptionStreamFactory.java | 16 +- .../OpenPgpMessageInputStream.java | 391 +++++++----------- .../SignatureInputStream.java | 12 +- ...ignatureCheck.java => SignatureCheck.java} | 6 +- 4 files changed, 170 insertions(+), 255 deletions(-) rename pgpainless-core/src/main/java/org/pgpainless/signature/consumer/{DetachedSignatureCheck.java => SignatureCheck.java} (90%) 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 5e6b2aec..0739eb19 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 @@ -60,7 +60,7 @@ import org.pgpainless.key.info.KeyRingInfo; import org.pgpainless.key.protection.SecretKeyRingProtector; import org.pgpainless.key.protection.UnlockSecretKey; import org.pgpainless.signature.SignatureUtils; -import org.pgpainless.signature.consumer.DetachedSignatureCheck; +import org.pgpainless.signature.consumer.SignatureCheck; import org.pgpainless.signature.consumer.OnePassSignatureCheck; import org.pgpainless.util.ArmoredInputStreamFactory; import org.pgpainless.util.Passphrase; @@ -79,7 +79,7 @@ public final class DecryptionStreamFactory { private final ConsumerOptions options; private final OpenPgpMetadata.Builder resultBuilder = OpenPgpMetadata.getBuilder(); private final List onePassSignatureChecks = new ArrayList<>(); - private final List detachedSignatureChecks = new ArrayList<>(); + private final List signatureChecks = new ArrayList<>(); private final Map onePassSignaturesWithMissingCert = new HashMap<>(); private static final PGPContentVerifierBuilderProvider verifierBuilderProvider = @@ -88,7 +88,7 @@ public final class DecryptionStreamFactory { public static DecryptionStream create(@Nonnull InputStream inputStream, - @Nonnull ConsumerOptions options) + @Nonnull ConsumerOptions options) throws PGPException, IOException { OpenPgpInputStream openPgpInputStream = new OpenPgpInputStream(inputStream); openPgpInputStream.reset(); @@ -134,9 +134,9 @@ public final class DecryptionStreamFactory { SubkeyIdentifier signingKeyIdentifier = new SubkeyIdentifier(signingKeyRing, signingKey.getKeyID()); try { signature.init(verifierBuilderProvider, signingKey); - DetachedSignatureCheck detachedSignature = - new DetachedSignatureCheck(signature, signingKeyRing, signingKeyIdentifier); - detachedSignatureChecks.add(detachedSignature); + SignatureCheck detachedSignature = + new SignatureCheck(signature, signingKeyRing, signingKeyIdentifier); + signatureChecks.add(detachedSignature); } catch (PGPException e) { SignatureValidationException ex = new SignatureValidationException( "Cannot verify detached signature made by " + signingKeyIdentifier + ".", e); @@ -212,7 +212,7 @@ public final class DecryptionStreamFactory { private InputStream wrapInVerifySignatureStream(InputStream bufferedIn, @Nullable PGPObjectFactory objectFactory) { return new SignatureInputStream.VerifySignatures( bufferedIn, objectFactory, onePassSignatureChecks, - onePassSignaturesWithMissingCert, detachedSignatureChecks, options, + onePassSignaturesWithMissingCert, signatureChecks, options, resultBuilder); } @@ -348,7 +348,7 @@ public final class DecryptionStreamFactory { } return new SignatureInputStream.VerifySignatures(literalDataInputStream, objectFactory, - onePassSignatureChecks, onePassSignaturesWithMissingCert, detachedSignatureChecks, options, resultBuilder) { + onePassSignatureChecks, onePassSignaturesWithMissingCert, signatureChecks, options, resultBuilder) { }; } diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java index f5ed556a..101a60c8 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java @@ -5,7 +5,6 @@ package org.pgpainless.decryption_verification; import java.io.BufferedInputStream; -import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; @@ -31,6 +30,7 @@ import org.bouncycastle.openpgp.PGPPublicKeyRing; import org.bouncycastle.openpgp.PGPSecretKey; import org.bouncycastle.openpgp.PGPSecretKeyRing; import org.bouncycastle.openpgp.PGPSignature; +import org.bouncycastle.openpgp.PGPSignatureList; import org.bouncycastle.openpgp.operator.PBEDataDecryptorFactory; import org.bouncycastle.openpgp.operator.PGPContentVerifierBuilderProvider; import org.bouncycastle.openpgp.operator.PublicKeyDataDecryptorFactory; @@ -44,6 +44,8 @@ import org.pgpainless.algorithm.SymmetricKeyAlgorithm; import org.pgpainless.decryption_verification.automaton.InputAlphabet; import org.pgpainless.decryption_verification.automaton.PDA; import org.pgpainless.decryption_verification.automaton.StackAlphabet; +import org.pgpainless.decryption_verification.cleartext_signatures.ClearsignedMessageUtil; +import org.pgpainless.decryption_verification.cleartext_signatures.MultiPassStrategy; import org.pgpainless.exception.MalformedOpenPgpMessageException; import org.pgpainless.exception.MessageNotIntegrityProtectedException; import org.pgpainless.exception.MissingDecryptionMethodException; @@ -56,7 +58,9 @@ import org.pgpainless.key.protection.SecretKeyRingProtector; import org.pgpainless.key.protection.UnlockSecretKey; import org.pgpainless.policy.Policy; import org.pgpainless.signature.SignatureUtils; -import org.pgpainless.signature.consumer.SignatureValidator; +import org.pgpainless.signature.consumer.OnePassSignatureCheck; +import org.pgpainless.signature.consumer.SignatureCheck; +import org.pgpainless.signature.consumer.SignatureVerifier; import org.pgpainless.util.ArmoredInputStreamFactory; import org.pgpainless.util.Passphrase; import org.pgpainless.util.SessionKey; @@ -93,7 +97,9 @@ public class OpenPgpMessageInputStream extends DecryptionStream { @Nonnull ConsumerOptions options, @Nonnull Policy policy) throws PGPException, IOException { - this(prepareInputStream(inputStream, options), options, new MessageMetadata.Message(), policy); + this( + prepareInputStream(inputStream, options, policy), + options, new MessageMetadata.Message(), policy); } protected OpenPgpMessageInputStream(@Nonnull InputStream inputStream, @@ -120,7 +126,20 @@ public class OpenPgpMessageInputStream extends DecryptionStream { consumePackets(); } - private static InputStream prepareInputStream(InputStream inputStream, ConsumerOptions options) throws IOException { + protected OpenPgpMessageInputStream(@Nonnull InputStream inputStream, + @Nonnull Policy policy, + @Nonnull ConsumerOptions options) { + super(OpenPgpMetadata.getBuilder()); + this.policy = policy; + this.options = options; + this.metadata = new MessageMetadata.Message(); + this.signatures = new Signatures(options); + this.signatures.addDetachedSignatures(options.getDetachedSignatures()); + this.packetInputStream = new TeeBCPGInputStream(BCPGInputStream.wrap(inputStream), signatures); + } + + private static InputStream prepareInputStream(InputStream inputStream, ConsumerOptions options, Policy policy) + throws IOException, PGPException { OpenPgpInputStream openPgpIn = new OpenPgpInputStream(inputStream); openPgpIn.reset(); @@ -131,7 +150,7 @@ public class OpenPgpMessageInputStream extends DecryptionStream { if (openPgpIn.isAsciiArmored()) { ArmoredInputStream armorIn = ArmoredInputStreamFactory.get(openPgpIn); if (armorIn.isClearText()) { - return armorIn; + return parseCleartextSignedMessage(armorIn, options, policy); } else { return armorIn; } @@ -140,6 +159,19 @@ public class OpenPgpMessageInputStream extends DecryptionStream { } } + private static DecryptionStream parseCleartextSignedMessage(ArmoredInputStream armorIn, ConsumerOptions options, Policy policy) + throws IOException, PGPException { + MultiPassStrategy multiPassStrategy = options.getMultiPassStrategy(); + PGPSignatureList signatures = ClearsignedMessageUtil.detachSignaturesFromInbandClearsignedMessage(armorIn, multiPassStrategy.getMessageOutputStream()); + + for (PGPSignature signature : signatures) { + options.addVerificationOfDetachedSignature(signature); + } + + options.forceNonOpenPgpData(); + return new OpenPgpMessageInputStream(multiPassStrategy.getMessageInputStream(), policy, options); + } + /** * Consume OpenPGP packets from the current {@link BCPGInputStream}. * Once an OpenPGP packet with nested data (Literal Data, Compressed Data, Encrypted Data) is reached, @@ -575,17 +607,58 @@ public class OpenPgpMessageInputStream extends DecryptionStream { } } + @Override + public OpenPgpMetadata getResult() { + MessageMetadata m = getMetadata(); + resultBuilder.setCompressionAlgorithm(m.getCompressionAlgorithm()); + resultBuilder.setModificationDate(m.getModificationDate()); + resultBuilder.setFileName(m.getFilename()); + resultBuilder.setFileEncoding(m.getFormat()); + resultBuilder.setSessionKey(m.getSessionKey()); + resultBuilder.setDecryptionKey(m.getDecryptionKey()); + + for (SignatureVerification accepted : m.getVerifiedDetachedSignatures()) { + resultBuilder.addVerifiedDetachedSignature(accepted); + } + for (SignatureVerification.Failure rejected : m.getRejectedDetachedSignatures()) { + resultBuilder.addInvalidDetachedSignature(rejected.getSignatureVerification(), rejected.getValidationException()); + } + + for (SignatureVerification accepted : m.getVerifiedInlineSignatures()) { + resultBuilder.addVerifiedInbandSignature(accepted); + } + for (SignatureVerification.Failure rejected : m.getRejectedInlineSignatures()) { + resultBuilder.addInvalidInbandSignature(rejected.getSignatureVerification(), rejected.getValidationException()); + } + + return resultBuilder.build(); + } + + static void log(String message) { + LOGGER.debug(message); + // CHECKSTYLE:OFF + System.out.println(message); + // CHECKSTYLE:ON + } + + static void log(String message, Throwable e) { + log(message); + // CHECKSTYLE:OFF + e.printStackTrace(); + // CHECKSTYLE:ON + } + // In 'OPS LIT("Foo") SIG', OPS is only updated with "Foo" // In 'OPS[1] OPS LIT("Foo") SIG SIG', OPS[1] (nested) is updated with OPS LIT("Foo") SIG. // Therefore, we need to handle the innermost signature layer differently when updating with Literal data. // Furthermore, For 'OPS COMP(LIT("Foo")) SIG', the signature is updated with "Foo". CHAOS!!! private static final class Signatures extends OutputStream { final ConsumerOptions options; - final List detachedSignatures; - final List prependedSignatures; - final List onePassSignatures; - final Stack> opsUpdateStack; - List literalOPS = new ArrayList<>(); + final List detachedSignatures; + final List prependedSignatures; + final List onePassSignatures; + final Stack> opsUpdateStack; + List literalOPS = new ArrayList<>(); final List correspondingSignatures; boolean isLiteral = true; @@ -605,31 +678,37 @@ public class OpenPgpMessageInputStream extends DecryptionStream { } void addDetachedSignature(PGPSignature signature) { - long keyId = SignatureUtils.determineIssuerKeyId(signature); - PGPPublicKeyRing certificate = findCertificate(keyId); - - if (certificate != null) { - initialize(signature, certificate, keyId); - this.detachedSignatures.add(new DetachedOrPrependedSignature(signature, certificate, keyId)); + SignatureCheck check = initializeSignature(signature); + if (check != null) { + detachedSignatures.add(check); } } void addPrependedSignature(PGPSignature signature) { + SignatureCheck check = initializeSignature(signature); + if (check != null) { + this.prependedSignatures.add(check); + } + } + + SignatureCheck initializeSignature(PGPSignature signature) { long keyId = SignatureUtils.determineIssuerKeyId(signature); PGPPublicKeyRing certificate = findCertificate(keyId); - - if (certificate != null) { - initialize(signature, certificate, keyId); - this.prependedSignatures.add(new DetachedOrPrependedSignature(signature, certificate, keyId)); + if (certificate == null) { + return null; } + + SubkeyIdentifier verifierKey = new SubkeyIdentifier(certificate, keyId); + initialize(signature, certificate, keyId); + return new SignatureCheck(signature, certificate, verifierKey); } void addOnePassSignature(PGPOnePassSignature signature) { PGPPublicKeyRing certificate = findCertificate(signature.getKeyID()); if (certificate != null) { - OnePassSignature ops = new OnePassSignature(signature, certificate, signature.getKeyID()); - ops.init(certificate); + OnePassSignatureCheck ops = new OnePassSignatureCheck(signature, certificate); + initialize(signature, certificate); onePassSignatures.add(ops); literalOPS.add(ops); @@ -641,40 +720,29 @@ public class OpenPgpMessageInputStream extends DecryptionStream { void addCorrespondingOnePassSignature(PGPSignature signature, MessageMetadata.Layer layer, Policy policy) { for (int i = onePassSignatures.size() - 1; i >= 0; i--) { - OnePassSignature onePassSignature = onePassSignatures.get(i); - if (onePassSignature.opSignature.getKeyID() != signature.getKeyID()) { - continue; - } - if (onePassSignature.finished) { + OnePassSignatureCheck onePassSignature = onePassSignatures.get(i); + if (onePassSignature.getOnePassSignature().getKeyID() != signature.getKeyID()) { continue; } - boolean correct = onePassSignature.verify(signature); + if (onePassSignature.getSignature() != null) { + continue; + } + + onePassSignature.setSignature(signature); SignatureVerification verification = new SignatureVerification(signature, - new SubkeyIdentifier(onePassSignature.certificate, onePassSignature.keyId)); - if (correct) { - PGPPublicKey signingKey = onePassSignature.certificate.getPublicKey(onePassSignature.keyId); - try { - checkSignatureValidity(signature, signingKey, policy); - layer.addVerifiedOnePassSignature(verification); - } catch (SignatureValidationException e) { - layer.addRejectedOnePassSignature(new SignatureVerification.Failure(verification, e)); - } - } else { - layer.addRejectedOnePassSignature(new SignatureVerification.Failure(verification, - new SignatureValidationException("Bad Signature."))); + new SubkeyIdentifier(onePassSignature.getVerificationKeys(), onePassSignature.getOnePassSignature().getKeyID())); + + try { + SignatureVerifier.verifyOnePassSignature(signature, onePassSignature.getVerificationKeys().getPublicKey(signature.getKeyID()), onePassSignature, policy); + layer.addVerifiedOnePassSignature(verification); + } catch (SignatureValidationException e) { + layer.addRejectedOnePassSignature(new SignatureVerification.Failure(verification, e)); } break; } } - boolean checkSignatureValidity(PGPSignature signature, PGPPublicKey signingKey, Policy policy) throws SignatureValidationException { - SignatureValidator.wasPossiblyMadeByKey(signingKey).verify(signature); - SignatureValidator.signatureStructureIsAcceptable(signingKey, policy).verify(signature); - SignatureValidator.signatureIsEffective().verify(signature); - return true; - } - void enterNesting() { opsUpdateStack.push(literalOPS); literalOPS = new ArrayList<>(); @@ -718,85 +786,75 @@ public class OpenPgpMessageInputStream extends DecryptionStream { } public void updateLiteral(byte b) { - for (OnePassSignature ops : literalOPS) { - ops.update(b); + for (OnePassSignatureCheck ops : literalOPS) { + ops.getOnePassSignature().update(b); } - for (DetachedOrPrependedSignature detached : detachedSignatures) { - detached.update(b); + for (SignatureCheck detached : detachedSignatures) { + detached.getSignature().update(b); } - for (DetachedOrPrependedSignature prepended : prependedSignatures) { - prepended.update(b); + for (SignatureCheck prepended : prependedSignatures) { + prepended.getSignature().update(b); } } public void updateLiteral(byte[] b, int off, int len) { - for (OnePassSignature ops : literalOPS) { - ops.update(b, off, len); + for (OnePassSignatureCheck ops : literalOPS) { + ops.getOnePassSignature().update(b, off, len); } - for (DetachedOrPrependedSignature detached : detachedSignatures) { - detached.update(b, off, len); + for (SignatureCheck detached : detachedSignatures) { + detached.getSignature().update(b, off, len); } - for (DetachedOrPrependedSignature prepended : prependedSignatures) { - prepended.update(b, off, len); + for (SignatureCheck prepended : prependedSignatures) { + prepended.getSignature().update(b, off, len); } } public void updatePacket(byte b) { for (int i = opsUpdateStack.size() - 1; i >= 0; i--) { - List nestedOPSs = opsUpdateStack.get(i); - for (OnePassSignature ops : nestedOPSs) { - ops.update(b); + List nestedOPSs = opsUpdateStack.get(i); + for (OnePassSignatureCheck ops : nestedOPSs) { + ops.getOnePassSignature().update(b); } } } public void updatePacket(byte[] buf, int off, int len) { for (int i = opsUpdateStack.size() - 1; i >= 0; i--) { - List nestedOPSs = opsUpdateStack.get(i); - for (OnePassSignature ops : nestedOPSs) { - ops.update(buf, off, len); + List nestedOPSs = opsUpdateStack.get(i); + for (OnePassSignatureCheck ops : nestedOPSs) { + ops.getOnePassSignature().update(buf, off, len); } } } public void finish(MessageMetadata.Layer layer, Policy policy) { - for (DetachedOrPrependedSignature detached : detachedSignatures) { - boolean correct = detached.verify(); - SignatureVerification verification = new SignatureVerification( - detached.signature, new SubkeyIdentifier(detached.certificate, detached.keyId)); - if (correct) { - try { - PGPPublicKey signingKey = detached.certificate.getPublicKey(detached.keyId); - checkSignatureValidity(detached.signature, signingKey, policy); - layer.addVerifiedDetachedSignature(verification); - } catch (SignatureValidationException e) { - layer.addRejectedDetachedSignature(new SignatureVerification.Failure(verification, e)); - } - } else { - layer.addRejectedDetachedSignature(new SignatureVerification.Failure( - verification, new SignatureValidationException("Incorrect Signature."))); + for (SignatureCheck detached : detachedSignatures) { + SignatureVerification verification = new SignatureVerification(detached.getSignature(), detached.getSigningKeyIdentifier()); + try { + SignatureVerifier.verifyInitializedSignature( + detached.getSignature(), + detached.getSigningKeyRing().getPublicKey(detached.getSigningKeyIdentifier().getKeyId()), + policy, detached.getSignature().getCreationTime()); + layer.addVerifiedDetachedSignature(verification); + } catch (SignatureValidationException e) { + layer.addRejectedDetachedSignature(new SignatureVerification.Failure(verification, e)); } } - for (DetachedOrPrependedSignature prepended : prependedSignatures) { - boolean correct = prepended.verify(); - SignatureVerification verification = new SignatureVerification( - prepended.signature, new SubkeyIdentifier(prepended.certificate, prepended.keyId)); - if (correct) { - try { - PGPPublicKey signingKey = prepended.certificate.getPublicKey(prepended.keyId); - checkSignatureValidity(prepended.signature, signingKey, policy); - layer.addVerifiedPrependedSignature(verification); - } catch (SignatureValidationException e) { - layer.addRejectedPrependedSignature(new SignatureVerification.Failure(verification, e)); - } - } else { - layer.addRejectedPrependedSignature(new SignatureVerification.Failure( - verification, new SignatureValidationException("Incorrect Signature."))); + for (SignatureCheck prepended : prependedSignatures) { + SignatureVerification verification = new SignatureVerification(prepended.getSignature(), prepended.getSigningKeyIdentifier()); + try { + SignatureVerifier.verifyInitializedSignature( + prepended.getSignature(), + prepended.getSigningKeyRing().getPublicKey(prepended.getSigningKeyIdentifier().getKeyId()), + policy, prepended.getSignature().getCreationTime()); + layer.addVerifiedPrependedSignature(verification); + } catch (SignatureValidationException e) { + layer.addRejectedPrependedSignature(new SignatureVerification.Failure(verification, e)); } } } @@ -822,148 +880,5 @@ public class OpenPgpMessageInputStream extends DecryptionStream { } } - static class DetachedOrPrependedSignature { - ByteArrayOutputStream bytes = new ByteArrayOutputStream(); - PGPSignature signature; - PGPPublicKeyRing certificate; - long keyId; - boolean finished; - boolean valid; - - DetachedOrPrependedSignature(PGPSignature signature, PGPPublicKeyRing certificate, long keyId) { - this.signature = signature; - this.certificate = certificate; - this.keyId = keyId; - } - - public void init(PGPPublicKeyRing certificate) { - initialize(signature, certificate, signature.getKeyID()); - } - - public boolean verify() { - if (finished) { - throw new IllegalStateException("Already finished."); - } - finished = true; - try { - valid = this.signature.verify(); - } catch (PGPException e) { - log("Cannot verify SIG " + signature.getKeyID()); - } - return valid; - } - - public void update(byte b) { - if (finished) { - throw new IllegalStateException("Already finished."); - } - signature.update(b); - bytes.write(b); - } - - public void update(byte[] bytes, int off, int len) { - if (finished) { - throw new IllegalStateException("Already finished."); - } - signature.update(bytes, off, len); - this.bytes.write(bytes, off, len); - } - } - - static class OnePassSignature { - ByteArrayOutputStream bytes = new ByteArrayOutputStream(); - PGPOnePassSignature opSignature; - PGPSignature signature; - PGPPublicKeyRing certificate; - long keyId; - boolean finished; - boolean valid; - - OnePassSignature(PGPOnePassSignature signature, PGPPublicKeyRing certificate, long keyId) { - this.opSignature = signature; - this.certificate = certificate; - this.keyId = keyId; - } - - public void init(PGPPublicKeyRing certificate) { - initialize(opSignature, certificate); - } - - public boolean verify(PGPSignature signature) { - if (finished) { - throw new IllegalStateException("Already finished."); - } - - if (this.opSignature.getKeyID() != signature.getKeyID()) { - // nope - return false; - } - this.signature = signature; - finished = true; - try { - valid = this.opSignature.verify(signature); - } catch (PGPException e) { - log("Cannot verify OPS " + signature.getKeyID()); - } - return valid; - } - - public void update(byte b) { - if (finished) { - throw new IllegalStateException("Already finished."); - } - opSignature.update(b); - bytes.write(b); - } - - public void update(byte[] bytes, int off, int len) { - if (finished) { - throw new IllegalStateException("Already finished."); - } - opSignature.update(bytes, off, len); - this.bytes.write(bytes, off, len); - } - } - } - - @Override - public OpenPgpMetadata getResult() { - MessageMetadata m = getMetadata(); - resultBuilder.setCompressionAlgorithm(m.getCompressionAlgorithm()); - resultBuilder.setModificationDate(m.getModificationDate()); - resultBuilder.setFileName(m.getFilename()); - resultBuilder.setFileEncoding(m.getFormat()); - resultBuilder.setSessionKey(m.getSessionKey()); - resultBuilder.setDecryptionKey(m.getDecryptionKey()); - - for (SignatureVerification accepted : m.getVerifiedDetachedSignatures()) { - resultBuilder.addVerifiedDetachedSignature(accepted); - } - for (SignatureVerification.Failure rejected : m.getRejectedDetachedSignatures()) { - resultBuilder.addInvalidDetachedSignature(rejected.getSignatureVerification(), rejected.getValidationException()); - } - - for (SignatureVerification accepted : m.getVerifiedInlineSignatures()) { - resultBuilder.addVerifiedInbandSignature(accepted); - } - for (SignatureVerification.Failure rejected : m.getRejectedInlineSignatures()) { - resultBuilder.addInvalidInbandSignature(rejected.getSignatureVerification(), rejected.getValidationException()); - } - - return resultBuilder.build(); - } - - static void log(String message) { - LOGGER.debug(message); - // CHECKSTYLE:OFF - System.out.println(message); - // CHECKSTYLE:ON - } - - static void log(String message, Throwable e) { - log(message); - // CHECKSTYLE:OFF - e.printStackTrace(); - // CHECKSTYLE:ON } } diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/SignatureInputStream.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/SignatureInputStream.java index 275acc17..70a2f4ef 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/SignatureInputStream.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/SignatureInputStream.java @@ -22,7 +22,7 @@ import org.pgpainless.PGPainless; import org.pgpainless.exception.SignatureValidationException; import org.pgpainless.policy.Policy; import org.pgpainless.signature.consumer.CertificateValidator; -import org.pgpainless.signature.consumer.DetachedSignatureCheck; +import org.pgpainless.signature.consumer.SignatureCheck; import org.pgpainless.signature.consumer.OnePassSignatureCheck; import org.pgpainless.signature.SignatureUtils; import org.slf4j.Logger; @@ -41,7 +41,7 @@ public abstract class SignatureInputStream extends FilterInputStream { private final PGPObjectFactory objectFactory; private final List opSignatures; private final Map opSignaturesWithMissingCert; - private final List detachedSignatures; + private final List detachedSignatures; private final ConsumerOptions options; private final OpenPgpMetadata.Builder resultBuilder; @@ -50,7 +50,7 @@ public abstract class SignatureInputStream extends FilterInputStream { @Nullable PGPObjectFactory objectFactory, List opSignatures, Map onePassSignaturesWithMissingCert, - List detachedSignatures, + List detachedSignatures, ConsumerOptions options, OpenPgpMetadata.Builder resultBuilder) { super(literalDataStream); @@ -170,7 +170,7 @@ public abstract class SignatureInputStream extends FilterInputStream { private void verifyDetachedSignatures() { Policy policy = PGPainless.getPolicy(); - for (DetachedSignatureCheck s : detachedSignatures) { + for (SignatureCheck s : detachedSignatures) { try { signatureWasCreatedInBounds(options.getVerifyNotBefore(), options.getVerifyNotAfter()).verify(s.getSignature()); @@ -200,13 +200,13 @@ public abstract class SignatureInputStream extends FilterInputStream { } private void updateDetachedSignatures(byte b) { - for (DetachedSignatureCheck detachedSignature : detachedSignatures) { + for (SignatureCheck detachedSignature : detachedSignatures) { detachedSignature.getSignature().update(b); } } private void updateDetachedSignatures(byte[] b, int off, int read) { - for (DetachedSignatureCheck detachedSignature : detachedSignatures) { + for (SignatureCheck detachedSignature : detachedSignatures) { detachedSignature.getSignature().update(b, off, read); } } diff --git a/pgpainless-core/src/main/java/org/pgpainless/signature/consumer/DetachedSignatureCheck.java b/pgpainless-core/src/main/java/org/pgpainless/signature/consumer/SignatureCheck.java similarity index 90% rename from pgpainless-core/src/main/java/org/pgpainless/signature/consumer/DetachedSignatureCheck.java rename to pgpainless-core/src/main/java/org/pgpainless/signature/consumer/SignatureCheck.java index a431c5de..bc9f1f0b 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/signature/consumer/DetachedSignatureCheck.java +++ b/pgpainless-core/src/main/java/org/pgpainless/signature/consumer/SignatureCheck.java @@ -13,19 +13,19 @@ import org.pgpainless.key.SubkeyIdentifier; * Tuple-class which bundles together a signature, the signing key that created the signature, * an identifier of the signing key and a record of whether the signature was verified. */ -public class DetachedSignatureCheck { +public class SignatureCheck { private final PGPSignature signature; private final PGPKeyRing signingKeyRing; private final SubkeyIdentifier signingKeyIdentifier; /** - * Create a new {@link DetachedSignatureCheck} object. + * Create a new {@link SignatureCheck} object. * * @param signature signature * @param signingKeyRing signing key that created the signature * @param signingKeyIdentifier identifier of the used signing key */ - public DetachedSignatureCheck(PGPSignature signature, PGPKeyRing signingKeyRing, SubkeyIdentifier signingKeyIdentifier) { + public SignatureCheck(PGPSignature signature, PGPKeyRing signingKeyRing, SubkeyIdentifier signingKeyIdentifier) { this.signature = signature; this.signingKeyRing = signingKeyRing; this.signingKeyIdentifier = signingKeyIdentifier; From 7da34c8329badb17e243dc4ebb937d480164822b Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 20 Oct 2022 15:35:15 +0200 Subject: [PATCH 047/763] Work on postponed keys --- .../OpenPgpMessageInputStream.java | 121 +++++++++++++++--- 1 file changed, 104 insertions(+), 17 deletions(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java index 101a60c8..dfb0df91 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java @@ -10,7 +10,9 @@ import java.io.InputStream; import java.io.OutputStream; import java.util.ArrayList; import java.util.Collection; +import java.util.HashSet; import java.util.List; +import java.util.Set; import java.util.Stack; import javax.annotation.Nonnull; @@ -49,6 +51,7 @@ import org.pgpainless.decryption_verification.cleartext_signatures.MultiPassStra import org.pgpainless.exception.MalformedOpenPgpMessageException; import org.pgpainless.exception.MessageNotIntegrityProtectedException; import org.pgpainless.exception.MissingDecryptionMethodException; +import org.pgpainless.exception.MissingPassphraseException; import org.pgpainless.exception.SignatureValidationException; import org.pgpainless.exception.UnacceptableAlgorithmException; import org.pgpainless.implementation.ImplementationFactory; @@ -60,6 +63,7 @@ import org.pgpainless.policy.Policy; import org.pgpainless.signature.SignatureUtils; import org.pgpainless.signature.consumer.OnePassSignatureCheck; import org.pgpainless.signature.consumer.SignatureCheck; +import org.pgpainless.signature.consumer.SignatureValidator; import org.pgpainless.signature.consumer.SignatureVerifier; import org.pgpainless.util.ArmoredInputStreamFactory; import org.pgpainless.util.Passphrase; @@ -127,8 +131,8 @@ public class OpenPgpMessageInputStream extends DecryptionStream { } protected OpenPgpMessageInputStream(@Nonnull InputStream inputStream, - @Nonnull Policy policy, - @Nonnull ConsumerOptions options) { + @Nonnull Policy policy, + @Nonnull ConsumerOptions options) { super(OpenPgpMetadata.getBuilder()); this.policy = policy; this.options = options; @@ -371,6 +375,7 @@ public class OpenPgpMessageInputStream extends DecryptionStream { } } + List> postponedDueToMissingPassphrase = new ArrayList<>(); // Try (known) secret keys for (PGPPublicKeyEncryptedData pkesk : esks.pkesks) { long keyId = pkesk.getKeyID(); @@ -378,7 +383,15 @@ public class OpenPgpMessageInputStream extends DecryptionStream { if (decryptionKeys == null) { continue; } + PGPSecretKey secretKey = decryptionKeys.getSecretKey(keyId); + SecretKeyRingProtector protector = options.getSecretKeyProtector(decryptionKeys); + // Postpone keys with missing passphrase + if (!protector.hasPassphraseFor(keyId)) { + postponedDueToMissingPassphrase.add(new Tuple<>(secretKey, pkesk)); + continue; + } + PGPSecretKey decryptionKey = decryptionKeys.getSecretKey(keyId); PGPPrivateKey privateKey = UnlockSecretKey.unlockSecretKey(decryptionKey, protector); @@ -408,7 +421,12 @@ public class OpenPgpMessageInputStream extends DecryptionStream { // try anonymous secret keys for (PGPPublicKeyEncryptedData pkesk : esks.anonPkesks) { for (Tuple decryptionKeyCandidate : findPotentialDecryptionKeys(pkesk)) { + PGPSecretKey secretKey = decryptionKeyCandidate.getB(); SecretKeyRingProtector protector = options.getSecretKeyProtector(decryptionKeyCandidate.getA()); + if (!protector.hasPassphraseFor(secretKey.getKeyID())) { + postponedDueToMissingPassphrase.add(new Tuple<>(secretKey, pkesk)); + continue; + } PGPPrivateKey privateKey = UnlockSecretKey.unlockSecretKey(decryptionKeyCandidate.getB(), protector); PublicKeyDataDecryptorFactory decryptorFactory = ImplementationFactory.getInstance() .getPublicKeyDataDecryptorFactory(privateKey); @@ -433,10 +451,64 @@ public class OpenPgpMessageInputStream extends DecryptionStream { } } + if (options.getMissingKeyPassphraseStrategy() == MissingKeyPassphraseStrategy.THROW_EXCEPTION) { + // Non-interactive mode: Throw an exception with all locked decryption keys + Set keyIds = new HashSet<>(); + for (Tuple k : postponedDueToMissingPassphrase) { + PGPSecretKey key = k.getA(); + keyIds.add(new SubkeyIdentifier(getDecryptionKey(key.getKeyID()), key.getKeyID())); + } + if (!keyIds.isEmpty()) { + throw new MissingPassphraseException(keyIds); + } + } else if (options.getMissingKeyPassphraseStrategy() == MissingKeyPassphraseStrategy.INTERACTIVE) { + for (PGPPublicKeyEncryptedData pkesk : esks.pkesks) { + // Interactive mode: Fire protector callbacks to get passphrases interactively + for (Tuple missingPassphrases : postponedDueToMissingPassphrase) { + PGPSecretKey secretKey = missingPassphrases.getA(); + long keyId = secretKey.getKeyID(); + PGPSecretKeyRing decryptionKey = getDecryptionKey(keyId); + SecretKeyRingProtector protector = options.getSecretKeyProtector(decryptionKey); + PGPPrivateKey privateKey = UnlockSecretKey.unlockSecretKey(secretKey, protector.getDecryptor(keyId)); + + PublicKeyDataDecryptorFactory decryptorFactory = ImplementationFactory.getInstance() + .getPublicKeyDataDecryptorFactory(privateKey); + + try { + InputStream decrypted = pkesk.getDataStream(decryptorFactory); + SessionKey sessionKey = new SessionKey(pkesk.getSessionKey(decryptorFactory)); + throwIfUnacceptable(sessionKey.getAlgorithm()); + + MessageMetadata.EncryptedData encryptedData = new MessageMetadata.EncryptedData( + SymmetricKeyAlgorithm.requireFromId(pkesk.getSymmetricAlgorithm(decryptorFactory)), + metadata.depth + 1); + encryptedData.decryptionKey = new SubkeyIdentifier(decryptionKey, keyId); + encryptedData.sessionKey = sessionKey; + + IntegrityProtectedInputStream integrityProtected = new IntegrityProtectedInputStream(decrypted, pkesk, options); + nestedInputStream = new OpenPgpMessageInputStream(buffer(integrityProtected), options, encryptedData, policy); + return true; + } catch (PGPException e) { + // hm :/ + } + } + } + } else { + throw new IllegalStateException("Invalid PostponedKeysStrategy set in consumer options."); + } + // we did not yet succeed in decrypting any session key :/ return false; } + private PGPSecretKey getDecryptionKey(PGPSecretKeyRing decryptionKeys, long keyId) { + KeyRingInfo info = PGPainless.inspectKeyRing(decryptionKeys); + if (info.getEncryptionSubkeys(EncryptionPurpose.ANY).contains(info.getPublicKey(keyId))) { + return info.getSecretKey(keyId); + } + return null; + } + private void throwIfUnacceptable(SymmetricKeyAlgorithm algorithm) throws UnacceptableAlgorithmException { if (!policy.getSymmetricKeyDecryptionAlgorithmPolicy().isAcceptable(algorithm)) { @@ -497,10 +569,12 @@ public class OpenPgpMessageInputStream extends DecryptionStream { collectMetadata(); nestedInputStream = null; - try { - consumePackets(); - } catch (PGPException e) { - throw new RuntimeException(e); + if (packetInputStream != null) { + try { + consumePackets(); + } catch (PGPException e) { + throw new RuntimeException(e); + } } signatures.finish(metadata, policy); } @@ -512,23 +586,26 @@ public class OpenPgpMessageInputStream extends DecryptionStream { throws IOException { if (nestedInputStream == null) { - automaton.assertValid(); + if (packetInputStream != null) { + automaton.assertValid(); + } return -1; } int r = nestedInputStream.read(b, off, len); if (r != -1) { signatures.updateLiteral(b, off, r); - } - else { + } else { nestedInputStream.close(); collectMetadata(); nestedInputStream = null; - try { - consumePackets(); - } catch (PGPException e) { - throw new RuntimeException(e); + if (packetInputStream != null) { + try { + consumePackets(); + } catch (PGPException e) { + throw new RuntimeException(e); + } } signatures.finish(metadata, policy); } @@ -539,7 +616,9 @@ public class OpenPgpMessageInputStream extends DecryptionStream { public void close() throws IOException { super.close(); if (closed) { - automaton.assertValid(); + if (packetInputStream != null) { + automaton.assertValid(); + } return; } @@ -555,9 +634,11 @@ public class OpenPgpMessageInputStream extends DecryptionStream { throw new RuntimeException(e); } - automaton.next(InputAlphabet.EndOfSequence); - automaton.assertValid(); - packetInputStream.close(); + if (packetInputStream != null) { + automaton.next(InputAlphabet.EndOfSequence); + automaton.assertValid(); + packetInputStream.close(); + } closed = true; } @@ -734,6 +815,8 @@ public class OpenPgpMessageInputStream extends DecryptionStream { new SubkeyIdentifier(onePassSignature.getVerificationKeys(), onePassSignature.getOnePassSignature().getKeyID())); try { + SignatureValidator.signatureWasCreatedInBounds(options.getVerifyNotBefore(), options.getVerifyNotAfter()) + .verify(signature); SignatureVerifier.verifyOnePassSignature(signature, onePassSignature.getVerificationKeys().getPublicKey(signature.getKeyID()), onePassSignature, policy); layer.addVerifiedOnePassSignature(verification); } catch (SignatureValidationException e) { @@ -835,6 +918,8 @@ public class OpenPgpMessageInputStream extends DecryptionStream { for (SignatureCheck detached : detachedSignatures) { SignatureVerification verification = new SignatureVerification(detached.getSignature(), detached.getSigningKeyIdentifier()); try { + SignatureValidator.signatureWasCreatedInBounds(options.getVerifyNotBefore(), options.getVerifyNotAfter()) + .verify(detached.getSignature()); SignatureVerifier.verifyInitializedSignature( detached.getSignature(), detached.getSigningKeyRing().getPublicKey(detached.getSigningKeyIdentifier().getKeyId()), @@ -848,6 +933,8 @@ public class OpenPgpMessageInputStream extends DecryptionStream { for (SignatureCheck prepended : prependedSignatures) { SignatureVerification verification = new SignatureVerification(prepended.getSignature(), prepended.getSigningKeyIdentifier()); try { + SignatureValidator.signatureWasCreatedInBounds(options.getVerifyNotBefore(), options.getVerifyNotAfter()) + .verify(prepended.getSignature()); SignatureVerifier.verifyInitializedSignature( prepended.getSignature(), prepended.getSigningKeyRing().getPublicKey(prepended.getSigningKeyIdentifier().getKeyId()), From d39d062a0df05b9a4b67ec6d4bb55a56e6cff334 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 21 Sep 2022 15:03:45 +0200 Subject: [PATCH 048/763] WIP: Explore Hardware Decryption --- .../ConsumerOptions.java | 6 +++ .../HardwareSecurity.java | 27 +++++++++++++ .../HardwareSecurityCallbackTest.java | 39 +++++++++++++++++++ 3 files changed, 72 insertions(+) create mode 100644 pgpainless-core/src/main/java/org/pgpainless/decryption_verification/HardwareSecurity.java create mode 100644 pgpainless-core/src/test/java/org/pgpainless/decryption_verification/HardwareSecurityCallbackTest.java diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/ConsumerOptions.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/ConsumerOptions.java index b6117a60..d57eff48 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/ConsumerOptions.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/ConsumerOptions.java @@ -48,6 +48,7 @@ public class ConsumerOptions { // Session key for decryption without passphrase/key private SessionKey sessionKey = null; + private HardwareSecurity.DecryptionCallback hardwareDecryptionCallback = null; private final Map decryptionKeys = new HashMap<>(); private final Set decryptionPassphrases = new HashSet<>(); @@ -238,6 +239,11 @@ public class ConsumerOptions { return this; } + public ConsumerOptions setHardwareDecryptionCallback(HardwareSecurity.DecryptionCallback callback) { + this.hardwareDecryptionCallback = callback; + return this; + } + public @Nonnull Set getDecryptionKeys() { return Collections.unmodifiableSet(decryptionKeys.keySet()); } diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/HardwareSecurity.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/HardwareSecurity.java new file mode 100644 index 00000000..bea14aef --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/HardwareSecurity.java @@ -0,0 +1,27 @@ +package org.pgpainless.decryption_verification; + +import org.bouncycastle.openpgp.PGPPublicKeyEncryptedData; +import org.pgpainless.util.SessionKey; + +public class HardwareSecurity { + + public interface DecryptionCallback { + + /** + * Delegate decryption of a Public-Key-Encrypted-Session-Key (PKESK) to an external API for dealing with + * hardware security modules such as smartcards or TPMs. + * + * If decryption fails for some reason, a subclass of the {@link HardwareSecurityException} is thrown. + * + * @param pkesk public-key-encrypted session key + * @return decrypted session key + * @throws HardwareSecurityException exception + */ + SessionKey decryptSessionKey(PGPPublicKeyEncryptedData pkesk) throws HardwareSecurityException; + + } + + public static class HardwareSecurityException extends Exception { + + } +} diff --git a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/HardwareSecurityCallbackTest.java b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/HardwareSecurityCallbackTest.java new file mode 100644 index 00000000..d731277e --- /dev/null +++ b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/HardwareSecurityCallbackTest.java @@ -0,0 +1,39 @@ +package org.pgpainless.decryption_verification; + +import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.PGPPublicKeyEncryptedData; +import org.junit.jupiter.api.Test; +import org.pgpainless.PGPainless; +import org.pgpainless.util.SessionKey; + +import java.io.ByteArrayInputStream; +import java.io.IOException; + +public class HardwareSecurityCallbackTest { + + @Test + public void test() throws PGPException, IOException { + PGPainless.decryptAndOrVerify() + .onInputStream(new ByteArrayInputStream(new byte[0])) + .withOptions(ConsumerOptions.get() + .setHardwareDecryptionCallback(new HardwareSecurity.DecryptionCallback() { + @Override + public SessionKey decryptSessionKey(PGPPublicKeyEncryptedData pkesk) throws HardwareSecurity.HardwareSecurityException { + /* + pkesk.getSessionKey(new PublicKeyDataDecryptorFactory() { + @Override + public byte[] recoverSessionData(int keyAlgorithm, byte[][] secKeyData) throws PGPException { + return new byte[0]; + } + + @Override + public PGPDataDecryptor createDataDecryptor(boolean withIntegrityPacket, int encAlgorithm, byte[] key) throws PGPException { + return null; + } + }); + */ + return null; + } + })); + } +} From 529c64cf43d62d9764d2ffa540a61b88aaafd622 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Fri, 23 Sep 2022 16:17:42 +0200 Subject: [PATCH 049/763] Implement exploratory support for custom decryption factories This may enable decryption of messages with hardware-backed keys --- .../ConsumerOptions.java | 24 ++++- .../DecryptionStreamFactory.java | 28 ++++++ .../HardwareSecurity.java | 91 ++++++++++++++++++- ...stomPublicKeyDataDecryptorFactoryTest.java | 86 ++++++++++++++++++ .../HardwareSecurityCallbackTest.java | 39 -------- 5 files changed, 221 insertions(+), 47 deletions(-) create mode 100644 pgpainless-core/src/test/java/org/pgpainless/decryption_verification/CustomPublicKeyDataDecryptorFactoryTest.java delete mode 100644 pgpainless-core/src/test/java/org/pgpainless/decryption_verification/HardwareSecurityCallbackTest.java diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/ConsumerOptions.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/ConsumerOptions.java index d57eff48..8f0576ee 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/ConsumerOptions.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/ConsumerOptions.java @@ -22,6 +22,7 @@ import org.bouncycastle.openpgp.PGPPublicKeyRingCollection; import org.bouncycastle.openpgp.PGPSecretKeyRing; import org.bouncycastle.openpgp.PGPSecretKeyRingCollection; import org.bouncycastle.openpgp.PGPSignature; +import org.bouncycastle.openpgp.operator.PublicKeyDataDecryptorFactory; import org.pgpainless.decryption_verification.cleartext_signatures.InMemoryMultiPassStrategy; import org.pgpainless.decryption_verification.cleartext_signatures.MultiPassStrategy; import org.pgpainless.key.protection.SecretKeyRingProtector; @@ -48,7 +49,7 @@ public class ConsumerOptions { // Session key for decryption without passphrase/key private SessionKey sessionKey = null; - private HardwareSecurity.DecryptionCallback hardwareDecryptionCallback = null; + private Map, PublicKeyDataDecryptorFactory> customPublicKeyDataDecryptorFactories = new HashMap<>(); private final Map decryptionKeys = new HashMap<>(); private final Set decryptionPassphrases = new HashSet<>(); @@ -239,11 +240,28 @@ public class ConsumerOptions { return this; } - public ConsumerOptions setHardwareDecryptionCallback(HardwareSecurity.DecryptionCallback callback) { - this.hardwareDecryptionCallback = callback; + /** + * Add a custom {@link PublicKeyDataDecryptorFactory} which enable decryption of messages, e.g. using + * hardware-backed secret keys. + * (See e.g. {@link org.pgpainless.decryption_verification.HardwareSecurity.HardwareDataDecryptorFactory}). + * + * The set of key-ids determines, whether the decryptor factory shall be consulted to decrypt a given session key. + * See for example {@link HardwareSecurity#getIdsOfHardwareBackedKeys(PGPSecretKeyRing)}. + * + * @param keyIds set of key-ids for which the factory shall be consulted + * @param decryptorFactory decryptor factory + * @return options + */ + public ConsumerOptions addCustomDecryptorFactory( + @Nonnull Set keyIds, @Nonnull PublicKeyDataDecryptorFactory decryptorFactory) { + this.customPublicKeyDataDecryptorFactories.put(keyIds, decryptorFactory); return this; } + Map, PublicKeyDataDecryptorFactory> getCustomDecryptorFactories() { + return new HashMap<>(customPublicKeyDataDecryptorFactories); + } + public @Nonnull Set getDecryptionKeys() { return Collections.unmodifiableSet(decryptionKeys.keySet()); } 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 0739eb19..ba837940 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 @@ -408,6 +408,34 @@ public final class DecryptionStreamFactory { } } + // Try custom PublicKeyDataDecryptorFactories (e.g. hardware-backed). + Map, PublicKeyDataDecryptorFactory> customFactories = options.getCustomDecryptorFactories(); + for (PGPPublicKeyEncryptedData publicKeyEncryptedData : publicKeyProtected) { + Long keyId = publicKeyEncryptedData.getKeyID(); + for (Set keyIds : customFactories.keySet()) { + if (!keyIds.contains(keyId)) { + continue; + } + + PublicKeyDataDecryptorFactory decryptorFactory = customFactories.get(keyIds); + try { + InputStream decryptedDataStream = publicKeyEncryptedData.getDataStream(decryptorFactory); + PGPSessionKey pgpSessionKey = publicKeyEncryptedData.getSessionKey(decryptorFactory); + SessionKey sessionKey = new SessionKey(pgpSessionKey); + resultBuilder.setSessionKey(sessionKey); + + throwIfAlgorithmIsRejected(sessionKey.getAlgorithm()); + + integrityProtectedEncryptedInputStream = + new IntegrityProtectedInputStream(decryptedDataStream, publicKeyEncryptedData, options); + + return integrityProtectedEncryptedInputStream; + } catch (PGPException e) { + LOGGER.debug("Decryption with custom PublicKeyDataDecryptorFactory failed", e); + } + } + } + // Then try decryption with public key encryption for (PGPPublicKeyEncryptedData publicKeyEncryptedData : publicKeyProtected) { PGPPrivateKey privateKey = null; diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/HardwareSecurity.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/HardwareSecurity.java index bea14aef..cc8cf598 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/HardwareSecurity.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/HardwareSecurity.java @@ -1,8 +1,23 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + package org.pgpainless.decryption_verification; -import org.bouncycastle.openpgp.PGPPublicKeyEncryptedData; -import org.pgpainless.util.SessionKey; +import org.bouncycastle.bcpg.S2K; +import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.PGPSecretKey; +import org.bouncycastle.openpgp.PGPSecretKeyRing; +import org.bouncycastle.openpgp.operator.PGPDataDecryptor; +import org.bouncycastle.openpgp.operator.PublicKeyDataDecryptorFactory; +import org.bouncycastle.openpgp.operator.bc.BcPublicKeyDataDecryptorFactory; +import java.util.HashSet; +import java.util.Set; + +/** + * Enable integration of hardware-backed OpenPGP keys. + */ public class HardwareSecurity { public interface DecryptionCallback { @@ -13,15 +28,81 @@ public class HardwareSecurity { * * If decryption fails for some reason, a subclass of the {@link HardwareSecurityException} is thrown. * - * @param pkesk public-key-encrypted session key + * @param keyAlgorithm algorithm + * @param sessionKeyData encrypted session key + * * @return decrypted session key * @throws HardwareSecurityException exception */ - SessionKey decryptSessionKey(PGPPublicKeyEncryptedData pkesk) throws HardwareSecurityException; + byte[] decryptSessionKey(int keyAlgorithm, byte[] sessionKeyData) + throws HardwareSecurityException; } - public static class HardwareSecurityException extends Exception { + /** + * Return the key-ids of all keys which appear to be stored on a hardware token / smartcard. + * + * @param secretKeys secret keys + * @return set of keys with S2K type DIVERT_TO_CARD or GNU_DUMMY_S2K + */ + public static Set getIdsOfHardwareBackedKeys(PGPSecretKeyRing secretKeys) { + Set hardwareBackedKeys = new HashSet<>(); + for (PGPSecretKey secretKey : secretKeys) { + S2K s2K = secretKey.getS2K(); + if (s2K == null) { + continue; + } + + int type = s2K.getType(); + // TODO: Is GNU_DUMMY_S2K appropriate? + if (type == S2K.GNU_PROTECTION_MODE_DIVERT_TO_CARD || type == S2K.GNU_DUMMY_S2K) { + hardwareBackedKeys.add(secretKey.getKeyID()); + } + } + return hardwareBackedKeys; + } + + /** + * Implementation of {@link PublicKeyDataDecryptorFactory} which delegates decryption of encrypted session keys + * to a {@link DecryptionCallback}. + * Users can provide such a callback to delegate decryption of messages to hardware security SDKs. + */ + public static class HardwareDataDecryptorFactory implements PublicKeyDataDecryptorFactory { + + private final DecryptionCallback callback; + // luckily we can instantiate the BcPublicKeyDataDecryptorFactory with null as argument. + private final PublicKeyDataDecryptorFactory factory = + new BcPublicKeyDataDecryptorFactory(null); + + /** + * Create a new {@link HardwareDataDecryptorFactory}. + * + * @param callback decryption callback + */ + public HardwareDataDecryptorFactory(DecryptionCallback callback) { + this.callback = callback; + } + + @Override + public byte[] recoverSessionData(int keyAlgorithm, byte[][] secKeyData) + throws PGPException { + try { + // delegate decryption to the callback + return callback.decryptSessionKey(keyAlgorithm, secKeyData[0]); + } catch (HardwareSecurityException e) { + throw new PGPException("Hardware-backed decryption failed.", e); + } + } + + @Override + public PGPDataDecryptor createDataDecryptor(boolean withIntegrityPacket, int encAlgorithm, byte[] key) + throws PGPException { + return factory.createDataDecryptor(withIntegrityPacket, encAlgorithm, key); + } + } + + public static class HardwareSecurityException + extends Exception { } } diff --git a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/CustomPublicKeyDataDecryptorFactoryTest.java b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/CustomPublicKeyDataDecryptorFactoryTest.java new file mode 100644 index 00000000..f507e02e --- /dev/null +++ b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/CustomPublicKeyDataDecryptorFactoryTest.java @@ -0,0 +1,86 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.decryption_verification; + +import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.PGPPrivateKey; +import org.bouncycastle.openpgp.PGPPublicKey; +import org.bouncycastle.openpgp.PGPPublicKeyRing; +import org.bouncycastle.openpgp.PGPSecretKey; +import org.bouncycastle.openpgp.PGPSecretKeyRing; +import org.bouncycastle.openpgp.operator.PublicKeyDataDecryptorFactory; +import org.bouncycastle.openpgp.operator.bc.BcPublicKeyDataDecryptorFactory; +import org.bouncycastle.util.io.Streams; +import org.junit.jupiter.api.Test; +import org.pgpainless.PGPainless; +import org.pgpainless.algorithm.EncryptionPurpose; +import org.pgpainless.encryption_signing.EncryptionOptions; +import org.pgpainless.encryption_signing.EncryptionStream; +import org.pgpainless.encryption_signing.ProducerOptions; +import org.pgpainless.key.info.KeyRingInfo; +import org.pgpainless.key.protection.UnlockSecretKey; +import org.pgpainless.util.Passphrase; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.security.InvalidAlgorithmParameterException; +import java.security.NoSuchAlgorithmException; +import java.util.Collections; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class CustomPublicKeyDataDecryptorFactoryTest { + + @Test + public void testDecryptionWithEmulatedHardwareDecryptionCallback() + throws PGPException, IOException, InvalidAlgorithmParameterException, NoSuchAlgorithmException { + PGPSecretKeyRing secretKey = PGPainless.generateKeyRing().modernKeyRing("Alice"); + PGPPublicKeyRing cert = PGPainless.extractCertificate(secretKey); + KeyRingInfo info = PGPainless.inspectKeyRing(secretKey); + PGPPublicKey encryptionKey = info.getEncryptionSubkeys(EncryptionPurpose.ANY).get(0); + + // Encrypt a test message + String plaintext = "Hello, World!\n"; + ByteArrayOutputStream ciphertextOut = new ByteArrayOutputStream(); + EncryptionStream encryptionStream = PGPainless.encryptAndOrSign() + .onOutputStream(ciphertextOut) + .withOptions(ProducerOptions.encrypt(EncryptionOptions.get() + .addRecipient(cert))); + encryptionStream.write(plaintext.getBytes(StandardCharsets.UTF_8)); + encryptionStream.close(); + + HardwareSecurity.DecryptionCallback hardwareDecryptionCallback = new HardwareSecurity.DecryptionCallback() { + @Override + public byte[] decryptSessionKey(int keyAlgorithm, byte[] sessionKeyData) + throws HardwareSecurity.HardwareSecurityException { + // Emulate hardware decryption. + try { + PGPSecretKey decryptionKey = secretKey.getSecretKey(encryptionKey.getKeyID()); + PGPPrivateKey privateKey = UnlockSecretKey.unlockSecretKey(decryptionKey, Passphrase.emptyPassphrase()); + PublicKeyDataDecryptorFactory internal = new BcPublicKeyDataDecryptorFactory(privateKey); + return internal.recoverSessionData(keyAlgorithm, new byte[][] {sessionKeyData}); + } catch (PGPException e) { + throw new HardwareSecurity.HardwareSecurityException(); + } + } + }; + + // Decrypt + DecryptionStream decryptionStream = PGPainless.decryptAndOrVerify() + .onInputStream(new ByteArrayInputStream(ciphertextOut.toByteArray())) + .withOptions(ConsumerOptions.get() + .addCustomDecryptorFactory( + Collections.singleton(encryptionKey.getKeyID()), + new HardwareSecurity.HardwareDataDecryptorFactory(hardwareDecryptionCallback))); + + ByteArrayOutputStream decryptedOut = new ByteArrayOutputStream(); + Streams.pipeAll(decryptionStream, decryptedOut); + decryptionStream.close(); + + assertEquals(plaintext, decryptedOut.toString()); + } +} diff --git a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/HardwareSecurityCallbackTest.java b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/HardwareSecurityCallbackTest.java deleted file mode 100644 index d731277e..00000000 --- a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/HardwareSecurityCallbackTest.java +++ /dev/null @@ -1,39 +0,0 @@ -package org.pgpainless.decryption_verification; - -import org.bouncycastle.openpgp.PGPException; -import org.bouncycastle.openpgp.PGPPublicKeyEncryptedData; -import org.junit.jupiter.api.Test; -import org.pgpainless.PGPainless; -import org.pgpainless.util.SessionKey; - -import java.io.ByteArrayInputStream; -import java.io.IOException; - -public class HardwareSecurityCallbackTest { - - @Test - public void test() throws PGPException, IOException { - PGPainless.decryptAndOrVerify() - .onInputStream(new ByteArrayInputStream(new byte[0])) - .withOptions(ConsumerOptions.get() - .setHardwareDecryptionCallback(new HardwareSecurity.DecryptionCallback() { - @Override - public SessionKey decryptSessionKey(PGPPublicKeyEncryptedData pkesk) throws HardwareSecurity.HardwareSecurityException { - /* - pkesk.getSessionKey(new PublicKeyDataDecryptorFactory() { - @Override - public byte[] recoverSessionData(int keyAlgorithm, byte[][] secKeyData) throws PGPException { - return new byte[0]; - } - - @Override - public PGPDataDecryptor createDataDecryptor(boolean withIntegrityPacket, int encAlgorithm, byte[] key) throws PGPException { - return null; - } - }); - */ - return null; - } - })); - } -} From 228918f96b3669c852919bf0160e9295dbdd7dd1 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 10 Oct 2022 18:01:30 +0200 Subject: [PATCH 050/763] Change HardwareSecurity DecryptionCallback to emit key-id --- .../ConsumerOptions.java | 15 +++----- .../CustomPublicKeyDataDecryptorFactory.java | 13 +++++++ .../DecryptionStreamFactory.java | 34 +++++++++---------- .../HardwareSecurity.java | 17 +++++++--- ...stomPublicKeyDataDecryptorFactoryTest.java | 8 ++--- 5 files changed, 51 insertions(+), 36 deletions(-) create mode 100644 pgpainless-core/src/main/java/org/pgpainless/decryption_verification/CustomPublicKeyDataDecryptorFactory.java diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/ConsumerOptions.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/ConsumerOptions.java index 8f0576ee..dba9fca7 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/ConsumerOptions.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/ConsumerOptions.java @@ -49,7 +49,7 @@ public class ConsumerOptions { // Session key for decryption without passphrase/key private SessionKey sessionKey = null; - private Map, PublicKeyDataDecryptorFactory> customPublicKeyDataDecryptorFactories = new HashMap<>(); + private Map customPublicKeyDataDecryptorFactories = new HashMap<>(); private final Map decryptionKeys = new HashMap<>(); private final Set decryptionPassphrases = new HashSet<>(); @@ -245,20 +245,15 @@ public class ConsumerOptions { * hardware-backed secret keys. * (See e.g. {@link org.pgpainless.decryption_verification.HardwareSecurity.HardwareDataDecryptorFactory}). * - * The set of key-ids determines, whether the decryptor factory shall be consulted to decrypt a given session key. - * See for example {@link HardwareSecurity#getIdsOfHardwareBackedKeys(PGPSecretKeyRing)}. - * - * @param keyIds set of key-ids for which the factory shall be consulted - * @param decryptorFactory decryptor factory + * @param factory decryptor factory * @return options */ - public ConsumerOptions addCustomDecryptorFactory( - @Nonnull Set keyIds, @Nonnull PublicKeyDataDecryptorFactory decryptorFactory) { - this.customPublicKeyDataDecryptorFactories.put(keyIds, decryptorFactory); + public ConsumerOptions addCustomDecryptorFactory(@Nonnull CustomPublicKeyDataDecryptorFactory factory) { + this.customPublicKeyDataDecryptorFactories.put(factory.getKeyId(), factory); return this; } - Map, PublicKeyDataDecryptorFactory> getCustomDecryptorFactories() { + Map getCustomDecryptorFactories() { return new HashMap<>(customPublicKeyDataDecryptorFactories); } diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/CustomPublicKeyDataDecryptorFactory.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/CustomPublicKeyDataDecryptorFactory.java new file mode 100644 index 00000000..43e8e723 --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/CustomPublicKeyDataDecryptorFactory.java @@ -0,0 +1,13 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.decryption_verification; + +import org.bouncycastle.openpgp.operator.PublicKeyDataDecryptorFactory; + +public interface CustomPublicKeyDataDecryptorFactory extends PublicKeyDataDecryptorFactory { + + long getKeyId(); + +} 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 ba837940..23e3e47d 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 @@ -409,30 +409,28 @@ public final class DecryptionStreamFactory { } // Try custom PublicKeyDataDecryptorFactories (e.g. hardware-backed). - Map, PublicKeyDataDecryptorFactory> customFactories = options.getCustomDecryptorFactories(); + Map customFactories = options.getCustomDecryptorFactories(); for (PGPPublicKeyEncryptedData publicKeyEncryptedData : publicKeyProtected) { Long keyId = publicKeyEncryptedData.getKeyID(); - for (Set keyIds : customFactories.keySet()) { - if (!keyIds.contains(keyId)) { - continue; - } + if (!customFactories.containsKey(keyId)) { + continue; + } - PublicKeyDataDecryptorFactory decryptorFactory = customFactories.get(keyIds); - try { - InputStream decryptedDataStream = publicKeyEncryptedData.getDataStream(decryptorFactory); - PGPSessionKey pgpSessionKey = publicKeyEncryptedData.getSessionKey(decryptorFactory); - SessionKey sessionKey = new SessionKey(pgpSessionKey); - resultBuilder.setSessionKey(sessionKey); + PublicKeyDataDecryptorFactory decryptorFactory = customFactories.get(keyId); + try { + InputStream decryptedDataStream = publicKeyEncryptedData.getDataStream(decryptorFactory); + PGPSessionKey pgpSessionKey = publicKeyEncryptedData.getSessionKey(decryptorFactory); + SessionKey sessionKey = new SessionKey(pgpSessionKey); + resultBuilder.setSessionKey(sessionKey); - throwIfAlgorithmIsRejected(sessionKey.getAlgorithm()); + throwIfAlgorithmIsRejected(sessionKey.getAlgorithm()); - integrityProtectedEncryptedInputStream = - new IntegrityProtectedInputStream(decryptedDataStream, publicKeyEncryptedData, options); + integrityProtectedEncryptedInputStream = + new IntegrityProtectedInputStream(decryptedDataStream, publicKeyEncryptedData, options); - return integrityProtectedEncryptedInputStream; - } catch (PGPException e) { - LOGGER.debug("Decryption with custom PublicKeyDataDecryptorFactory failed", e); - } + return integrityProtectedEncryptedInputStream; + } catch (PGPException e) { + LOGGER.debug("Decryption with custom PublicKeyDataDecryptorFactory failed", e); } } diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/HardwareSecurity.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/HardwareSecurity.java index cc8cf598..d5bf60db 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/HardwareSecurity.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/HardwareSecurity.java @@ -28,13 +28,14 @@ public class HardwareSecurity { * * If decryption fails for some reason, a subclass of the {@link HardwareSecurityException} is thrown. * + * @param keyId id of the key * @param keyAlgorithm algorithm * @param sessionKeyData encrypted session key * * @return decrypted session key * @throws HardwareSecurityException exception */ - byte[] decryptSessionKey(int keyAlgorithm, byte[] sessionKeyData) + byte[] decryptSessionKey(long keyId, int keyAlgorithm, byte[] sessionKeyData) throws HardwareSecurityException; } @@ -67,20 +68,22 @@ public class HardwareSecurity { * to a {@link DecryptionCallback}. * Users can provide such a callback to delegate decryption of messages to hardware security SDKs. */ - public static class HardwareDataDecryptorFactory implements PublicKeyDataDecryptorFactory { + public static class HardwareDataDecryptorFactory implements CustomPublicKeyDataDecryptorFactory { private final DecryptionCallback callback; // luckily we can instantiate the BcPublicKeyDataDecryptorFactory with null as argument. private final PublicKeyDataDecryptorFactory factory = new BcPublicKeyDataDecryptorFactory(null); + private long keyId; /** * Create a new {@link HardwareDataDecryptorFactory}. * * @param callback decryption callback */ - public HardwareDataDecryptorFactory(DecryptionCallback callback) { + public HardwareDataDecryptorFactory(long keyId, DecryptionCallback callback) { this.callback = callback; + this.keyId = keyId; } @Override @@ -88,7 +91,7 @@ public class HardwareSecurity { throws PGPException { try { // delegate decryption to the callback - return callback.decryptSessionKey(keyAlgorithm, secKeyData[0]); + return callback.decryptSessionKey(keyId, keyAlgorithm, secKeyData[0]); } catch (HardwareSecurityException e) { throw new PGPException("Hardware-backed decryption failed.", e); } @@ -99,10 +102,16 @@ public class HardwareSecurity { throws PGPException { return factory.createDataDecryptor(withIntegrityPacket, encAlgorithm, key); } + + @Override + public long getKeyId() { + return keyId; + } } public static class HardwareSecurityException extends Exception { } + } diff --git a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/CustomPublicKeyDataDecryptorFactoryTest.java b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/CustomPublicKeyDataDecryptorFactoryTest.java index f507e02e..4ea894e5 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/CustomPublicKeyDataDecryptorFactoryTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/CustomPublicKeyDataDecryptorFactoryTest.java @@ -29,7 +29,6 @@ import java.io.IOException; import java.nio.charset.StandardCharsets; import java.security.InvalidAlgorithmParameterException; import java.security.NoSuchAlgorithmException; -import java.util.Collections; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -55,7 +54,7 @@ public class CustomPublicKeyDataDecryptorFactoryTest { HardwareSecurity.DecryptionCallback hardwareDecryptionCallback = new HardwareSecurity.DecryptionCallback() { @Override - public byte[] decryptSessionKey(int keyAlgorithm, byte[] sessionKeyData) + public byte[] decryptSessionKey(long keyId, int keyAlgorithm, byte[] sessionKeyData) throws HardwareSecurity.HardwareSecurityException { // Emulate hardware decryption. try { @@ -74,8 +73,9 @@ public class CustomPublicKeyDataDecryptorFactoryTest { .onInputStream(new ByteArrayInputStream(ciphertextOut.toByteArray())) .withOptions(ConsumerOptions.get() .addCustomDecryptorFactory( - Collections.singleton(encryptionKey.getKeyID()), - new HardwareSecurity.HardwareDataDecryptorFactory(hardwareDecryptionCallback))); + new HardwareSecurity.HardwareDataDecryptorFactory( + encryptionKey.getKeyID(), + hardwareDecryptionCallback))); ByteArrayOutputStream decryptedOut = new ByteArrayOutputStream(); Streams.pipeAll(decryptionStream, decryptedOut); From cfd3f77491e6b0b15b01ffc2f466c6fd8c2a3827 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 10 Oct 2022 18:08:43 +0200 Subject: [PATCH 051/763] Make map final --- .../org/pgpainless/decryption_verification/ConsumerOptions.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/ConsumerOptions.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/ConsumerOptions.java index dba9fca7..0f02aba0 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/ConsumerOptions.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/ConsumerOptions.java @@ -49,7 +49,7 @@ public class ConsumerOptions { // Session key for decryption without passphrase/key private SessionKey sessionKey = null; - private Map customPublicKeyDataDecryptorFactories = new HashMap<>(); + private final Map customPublicKeyDataDecryptorFactories = new HashMap<>(); private final Map decryptionKeys = new HashMap<>(); private final Set decryptionPassphrases = new HashSet<>(); From a39c6bc881f2763d6ea812b95d94341918efe3f7 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 20 Oct 2022 16:07:31 +0200 Subject: [PATCH 052/763] Identify custom decryptor factories by subkey id --- .../ConsumerOptions.java | 7 ++++--- .../CustomPublicKeyDataDecryptorFactory.java | 3 ++- .../DecryptionStreamFactory.java | 2 +- .../HardwareSecurity.java | 19 +++++++++++++------ ...stomPublicKeyDataDecryptorFactoryTest.java | 3 ++- 5 files changed, 22 insertions(+), 12 deletions(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/ConsumerOptions.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/ConsumerOptions.java index 0f02aba0..f8f59d36 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/ConsumerOptions.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/ConsumerOptions.java @@ -25,6 +25,7 @@ import org.bouncycastle.openpgp.PGPSignature; import org.bouncycastle.openpgp.operator.PublicKeyDataDecryptorFactory; import org.pgpainless.decryption_verification.cleartext_signatures.InMemoryMultiPassStrategy; import org.pgpainless.decryption_verification.cleartext_signatures.MultiPassStrategy; +import org.pgpainless.key.SubkeyIdentifier; import org.pgpainless.key.protection.SecretKeyRingProtector; import org.pgpainless.signature.SignatureUtils; import org.pgpainless.util.Passphrase; @@ -49,7 +50,7 @@ public class ConsumerOptions { // Session key for decryption without passphrase/key private SessionKey sessionKey = null; - private final Map customPublicKeyDataDecryptorFactories = new HashMap<>(); + private final Map customPublicKeyDataDecryptorFactories = new HashMap<>(); private final Map decryptionKeys = new HashMap<>(); private final Set decryptionPassphrases = new HashSet<>(); @@ -249,11 +250,11 @@ public class ConsumerOptions { * @return options */ public ConsumerOptions addCustomDecryptorFactory(@Nonnull CustomPublicKeyDataDecryptorFactory factory) { - this.customPublicKeyDataDecryptorFactories.put(factory.getKeyId(), factory); + this.customPublicKeyDataDecryptorFactories.put(factory.getSubkeyIdentifier(), factory); return this; } - Map getCustomDecryptorFactories() { + Map getCustomDecryptorFactories() { return new HashMap<>(customPublicKeyDataDecryptorFactories); } diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/CustomPublicKeyDataDecryptorFactory.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/CustomPublicKeyDataDecryptorFactory.java index 43e8e723..37dc10a9 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/CustomPublicKeyDataDecryptorFactory.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/CustomPublicKeyDataDecryptorFactory.java @@ -5,9 +5,10 @@ package org.pgpainless.decryption_verification; import org.bouncycastle.openpgp.operator.PublicKeyDataDecryptorFactory; +import org.pgpainless.key.SubkeyIdentifier; public interface CustomPublicKeyDataDecryptorFactory extends PublicKeyDataDecryptorFactory { - long getKeyId(); + SubkeyIdentifier getSubkeyIdentifier(); } 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 23e3e47d..451750ee 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 @@ -409,7 +409,7 @@ public final class DecryptionStreamFactory { } // Try custom PublicKeyDataDecryptorFactories (e.g. hardware-backed). - Map customFactories = options.getCustomDecryptorFactories(); + Map customFactories = options.getCustomDecryptorFactories(); for (PGPPublicKeyEncryptedData publicKeyEncryptedData : publicKeyProtected) { Long keyId = publicKeyEncryptedData.getKeyID(); if (!customFactories.containsKey(keyId)) { diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/HardwareSecurity.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/HardwareSecurity.java index d5bf60db..f234ff00 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/HardwareSecurity.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/HardwareSecurity.java @@ -11,6 +11,7 @@ import org.bouncycastle.openpgp.PGPSecretKeyRing; import org.bouncycastle.openpgp.operator.PGPDataDecryptor; import org.bouncycastle.openpgp.operator.PublicKeyDataDecryptorFactory; import org.bouncycastle.openpgp.operator.bc.BcPublicKeyDataDecryptorFactory; +import org.pgpainless.key.SubkeyIdentifier; import java.util.HashSet; import java.util.Set; @@ -74,16 +75,16 @@ public class HardwareSecurity { // luckily we can instantiate the BcPublicKeyDataDecryptorFactory with null as argument. private final PublicKeyDataDecryptorFactory factory = new BcPublicKeyDataDecryptorFactory(null); - private long keyId; + private SubkeyIdentifier subkey; /** * Create a new {@link HardwareDataDecryptorFactory}. * * @param callback decryption callback */ - public HardwareDataDecryptorFactory(long keyId, DecryptionCallback callback) { + public HardwareDataDecryptorFactory(SubkeyIdentifier subkeyIdentifier, DecryptionCallback callback) { this.callback = callback; - this.keyId = keyId; + this.subkey = subkeyIdentifier; } @Override @@ -91,7 +92,7 @@ public class HardwareSecurity { throws PGPException { try { // delegate decryption to the callback - return callback.decryptSessionKey(keyId, keyAlgorithm, secKeyData[0]); + return callback.decryptSessionKey(subkey.getSubkeyId(), keyAlgorithm, secKeyData[0]); } catch (HardwareSecurityException e) { throw new PGPException("Hardware-backed decryption failed.", e); } @@ -104,8 +105,14 @@ public class HardwareSecurity { } @Override - public long getKeyId() { - return keyId; + public PGPDataDecryptor createDataDecryptor(int aeadAlgorithm, byte[] iv, int chunkSize, int encAlgorithm, byte[] key) + throws PGPException { + return factory.createDataDecryptor(aeadAlgorithm, iv, chunkSize, encAlgorithm, key); + } + + @Override + public SubkeyIdentifier getSubkeyIdentifier() { + return subkey; } } diff --git a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/CustomPublicKeyDataDecryptorFactoryTest.java b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/CustomPublicKeyDataDecryptorFactoryTest.java index 4ea894e5..73c3bf56 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/CustomPublicKeyDataDecryptorFactoryTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/CustomPublicKeyDataDecryptorFactoryTest.java @@ -19,6 +19,7 @@ import org.pgpainless.algorithm.EncryptionPurpose; import org.pgpainless.encryption_signing.EncryptionOptions; import org.pgpainless.encryption_signing.EncryptionStream; import org.pgpainless.encryption_signing.ProducerOptions; +import org.pgpainless.key.SubkeyIdentifier; import org.pgpainless.key.info.KeyRingInfo; import org.pgpainless.key.protection.UnlockSecretKey; import org.pgpainless.util.Passphrase; @@ -74,7 +75,7 @@ public class CustomPublicKeyDataDecryptorFactoryTest { .withOptions(ConsumerOptions.get() .addCustomDecryptorFactory( new HardwareSecurity.HardwareDataDecryptorFactory( - encryptionKey.getKeyID(), + new SubkeyIdentifier(cert, encryptionKey.getKeyID()), hardwareDecryptionCallback))); ByteArrayOutputStream decryptedOut = new ByteArrayOutputStream(); From 3d5916c545a125369141e5cde0fe535a7507febb Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 20 Oct 2022 16:08:01 +0200 Subject: [PATCH 053/763] Implement custom decryptor factories in pda --- .../OpenPgpMessageInputStream.java | 122 +++++++++--------- 1 file changed, 61 insertions(+), 61 deletions(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java index dfb0df91..bba7b396 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java @@ -314,6 +314,20 @@ public class OpenPgpMessageInputStream extends DecryptionStream { SortedESKs esks = new SortedESKs(encDataList); + // Try custom decryptor factory + for (SubkeyIdentifier subkeyIdentifier : options.getCustomDecryptorFactories().keySet()) { + PublicKeyDataDecryptorFactory decryptorFactory = options.getCustomDecryptorFactories().get(subkeyIdentifier); + for (PGPPublicKeyEncryptedData pkesk : esks.pkesks) { + if (pkesk.getKeyID() != subkeyIdentifier.getSubkeyId()) { + continue; + } + + if (decryptPKESKAndStream(subkeyIdentifier, decryptorFactory, pkesk)) { + return true; + } + } + } + // Try session key if (options.getSessionKey() != null) { SessionKey sessionKey = options.getSessionKey(); @@ -357,20 +371,8 @@ public class OpenPgpMessageInputStream extends DecryptionStream { PBEDataDecryptorFactory decryptorFactory = ImplementationFactory.getInstance() .getPBEDataDecryptorFactory(passphrase); - try { - InputStream decrypted = skesk.getDataStream(decryptorFactory); - SessionKey sessionKey = new SessionKey(skesk.getSessionKey(decryptorFactory)); - throwIfUnacceptable(sessionKey.getAlgorithm()); - MessageMetadata.EncryptedData encryptedData = new MessageMetadata.EncryptedData( - sessionKey.getAlgorithm(), metadata.depth + 1); - encryptedData.sessionKey = sessionKey; - IntegrityProtectedInputStream integrityProtected = new IntegrityProtectedInputStream(decrypted, skesk, options); - nestedInputStream = new OpenPgpMessageInputStream(buffer(integrityProtected), options, encryptedData, policy); + if (decryptSKESKAndStream(skesk, decryptorFactory)) { return true; - } catch (UnacceptableAlgorithmException e) { - throw e; - } catch (PGPException e) { - // Password mismatch? } } } @@ -393,28 +395,13 @@ public class OpenPgpMessageInputStream extends DecryptionStream { } PGPSecretKey decryptionKey = decryptionKeys.getSecretKey(keyId); + SubkeyIdentifier decryptionKeyId = new SubkeyIdentifier(decryptionKeys, decryptionKey.getKeyID()); PGPPrivateKey privateKey = UnlockSecretKey.unlockSecretKey(decryptionKey, protector); PublicKeyDataDecryptorFactory decryptorFactory = ImplementationFactory.getInstance() .getPublicKeyDataDecryptorFactory(privateKey); - try { - InputStream decrypted = pkesk.getDataStream(decryptorFactory); - SessionKey sessionKey = new SessionKey(pkesk.getSessionKey(decryptorFactory)); - throwIfUnacceptable(sessionKey.getAlgorithm()); - - MessageMetadata.EncryptedData encryptedData = new MessageMetadata.EncryptedData( - SymmetricKeyAlgorithm.requireFromId(pkesk.getSymmetricAlgorithm(decryptorFactory)), - metadata.depth + 1); - encryptedData.decryptionKey = new SubkeyIdentifier(decryptionKeys, decryptionKey.getKeyID()); - encryptedData.sessionKey = sessionKey; - - IntegrityProtectedInputStream integrityProtected = new IntegrityProtectedInputStream(decrypted, pkesk, options); - nestedInputStream = new OpenPgpMessageInputStream(buffer(integrityProtected), options, encryptedData, policy); + if (decryptPKESKAndStream(decryptionKeyId, decryptorFactory, pkesk)) { return true; - } catch (UnacceptableAlgorithmException e) { - throw e; - } catch (PGPException e) { - } } @@ -430,23 +417,10 @@ public class OpenPgpMessageInputStream extends DecryptionStream { PGPPrivateKey privateKey = UnlockSecretKey.unlockSecretKey(decryptionKeyCandidate.getB(), protector); PublicKeyDataDecryptorFactory decryptorFactory = ImplementationFactory.getInstance() .getPublicKeyDataDecryptorFactory(privateKey); + SubkeyIdentifier decryptionKeyId = new SubkeyIdentifier(decryptionKeyCandidate.getA(), privateKey.getKeyID()); - try { - InputStream decrypted = pkesk.getDataStream(decryptorFactory); - SessionKey sessionKey = new SessionKey(pkesk.getSessionKey(decryptorFactory)); - throwIfUnacceptable(sessionKey.getAlgorithm()); - - MessageMetadata.EncryptedData encryptedData = new MessageMetadata.EncryptedData( - SymmetricKeyAlgorithm.requireFromId(pkesk.getSymmetricAlgorithm(decryptorFactory)), - metadata.depth + 1); - encryptedData.decryptionKey = new SubkeyIdentifier(decryptionKeyCandidate.getA(), privateKey.getKeyID()); - encryptedData.sessionKey = sessionKey; - - IntegrityProtectedInputStream integrityProtected = new IntegrityProtectedInputStream(decrypted, pkesk, options); - nestedInputStream = new OpenPgpMessageInputStream(buffer(integrityProtected), options, encryptedData, policy); + if (decryptPKESKAndStream(decryptionKeyId, decryptorFactory, pkesk)) { return true; - } catch (PGPException e) { - // hm :/ } } } @@ -470,26 +444,12 @@ public class OpenPgpMessageInputStream extends DecryptionStream { PGPSecretKeyRing decryptionKey = getDecryptionKey(keyId); SecretKeyRingProtector protector = options.getSecretKeyProtector(decryptionKey); PGPPrivateKey privateKey = UnlockSecretKey.unlockSecretKey(secretKey, protector.getDecryptor(keyId)); - + SubkeyIdentifier decryptionKeyId = new SubkeyIdentifier(decryptionKey, keyId); PublicKeyDataDecryptorFactory decryptorFactory = ImplementationFactory.getInstance() .getPublicKeyDataDecryptorFactory(privateKey); - try { - InputStream decrypted = pkesk.getDataStream(decryptorFactory); - SessionKey sessionKey = new SessionKey(pkesk.getSessionKey(decryptorFactory)); - throwIfUnacceptable(sessionKey.getAlgorithm()); - - MessageMetadata.EncryptedData encryptedData = new MessageMetadata.EncryptedData( - SymmetricKeyAlgorithm.requireFromId(pkesk.getSymmetricAlgorithm(decryptorFactory)), - metadata.depth + 1); - encryptedData.decryptionKey = new SubkeyIdentifier(decryptionKey, keyId); - encryptedData.sessionKey = sessionKey; - - IntegrityProtectedInputStream integrityProtected = new IntegrityProtectedInputStream(decrypted, pkesk, options); - nestedInputStream = new OpenPgpMessageInputStream(buffer(integrityProtected), options, encryptedData, policy); + if (decryptPKESKAndStream(decryptionKeyId, decryptorFactory, pkesk)) { return true; - } catch (PGPException e) { - // hm :/ } } } @@ -501,6 +461,46 @@ public class OpenPgpMessageInputStream extends DecryptionStream { return false; } + private boolean decryptSKESKAndStream(PGPPBEEncryptedData skesk, PBEDataDecryptorFactory decryptorFactory) throws IOException, UnacceptableAlgorithmException { + try { + InputStream decrypted = skesk.getDataStream(decryptorFactory); + SessionKey sessionKey = new SessionKey(skesk.getSessionKey(decryptorFactory)); + throwIfUnacceptable(sessionKey.getAlgorithm()); + MessageMetadata.EncryptedData encryptedData = new MessageMetadata.EncryptedData( + sessionKey.getAlgorithm(), metadata.depth + 1); + encryptedData.sessionKey = sessionKey; + IntegrityProtectedInputStream integrityProtected = new IntegrityProtectedInputStream(decrypted, skesk, options); + nestedInputStream = new OpenPgpMessageInputStream(buffer(integrityProtected), options, encryptedData, policy); + return true; + } catch (UnacceptableAlgorithmException e) { + throw e; + } catch (PGPException e) { + // Password mismatch? + } + return false; + } + + private boolean decryptPKESKAndStream(SubkeyIdentifier subkeyIdentifier, PublicKeyDataDecryptorFactory decryptorFactory, PGPPublicKeyEncryptedData pkesk) throws IOException { + try { + InputStream decrypted = pkesk.getDataStream(decryptorFactory); + SessionKey sessionKey = new SessionKey(pkesk.getSessionKey(decryptorFactory)); + throwIfUnacceptable(sessionKey.getAlgorithm()); + + MessageMetadata.EncryptedData encryptedData = new MessageMetadata.EncryptedData( + SymmetricKeyAlgorithm.requireFromId(pkesk.getSymmetricAlgorithm(decryptorFactory)), + metadata.depth + 1); + encryptedData.decryptionKey = subkeyIdentifier; + encryptedData.sessionKey = sessionKey; + + IntegrityProtectedInputStream integrityProtected = new IntegrityProtectedInputStream(decrypted, pkesk, options); + nestedInputStream = new OpenPgpMessageInputStream(buffer(integrityProtected), options, encryptedData, policy); + return true; + } catch (PGPException e) { + + } + return false; + } + private PGPSecretKey getDecryptionKey(PGPSecretKeyRing decryptionKeys, long keyId) { KeyRingInfo info = PGPainless.inspectKeyRing(decryptionKeys); if (info.getEncryptionSubkeys(EncryptionPurpose.ANY).contains(info.getPublicKey(keyId))) { From a9993fd86699bcaf0790dc7f5d8944d2a0f4ccbd Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 20 Oct 2022 16:13:18 +0200 Subject: [PATCH 054/763] Throw UnacceptableAlgEx for unencrypted encData --- .../decryption_verification/OpenPgpMessageInputStream.java | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java index bba7b396..969f3717 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java @@ -480,7 +480,10 @@ public class OpenPgpMessageInputStream extends DecryptionStream { return false; } - private boolean decryptPKESKAndStream(SubkeyIdentifier subkeyIdentifier, PublicKeyDataDecryptorFactory decryptorFactory, PGPPublicKeyEncryptedData pkesk) throws IOException { + private boolean decryptPKESKAndStream(SubkeyIdentifier subkeyIdentifier, + PublicKeyDataDecryptorFactory decryptorFactory, + PGPPublicKeyEncryptedData pkesk) + throws IOException, UnacceptableAlgorithmException { try { InputStream decrypted = pkesk.getDataStream(decryptorFactory); SessionKey sessionKey = new SessionKey(pkesk.getSessionKey(decryptorFactory)); @@ -495,6 +498,8 @@ public class OpenPgpMessageInputStream extends DecryptionStream { IntegrityProtectedInputStream integrityProtected = new IntegrityProtectedInputStream(decrypted, pkesk, options); nestedInputStream = new OpenPgpMessageInputStream(buffer(integrityProtected), options, encryptedData, policy); return true; + } catch (UnacceptableAlgorithmException e) { + throw e; } catch (PGPException e) { } From b7acb2a59c98690a5582db351730afe33a3c5311 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 20 Oct 2022 19:35:25 +0200 Subject: [PATCH 055/763] Enable logging in tests --- pgpainless-core/src/test/resources/logback-test.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pgpainless-core/src/test/resources/logback-test.xml b/pgpainless-core/src/test/resources/logback-test.xml index 7e4c3194..bb16e293 100644 --- a/pgpainless-core/src/test/resources/logback-test.xml +++ b/pgpainless-core/src/test/resources/logback-test.xml @@ -19,7 +19,7 @@ SPDX-License-Identifier: Apache-2.0 - + From a27c0ff36e10842ffc8c956e5a3b67afda549242 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 20 Oct 2022 19:35:50 +0200 Subject: [PATCH 056/763] Add detailled logging to OpenPgpMessageInputStream --- .../OpenPgpMessageInputStream.java | 84 +++++++++++++++---- .../automaton/PDA.java | 13 ++- .../org/pgpainless/key/util/KeyIdUtil.java | 4 + 3 files changed, 82 insertions(+), 19 deletions(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java index 969f3717..1dbadbbe 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java @@ -59,6 +59,7 @@ import org.pgpainless.key.SubkeyIdentifier; import org.pgpainless.key.info.KeyRingInfo; import org.pgpainless.key.protection.SecretKeyRingProtector; import org.pgpainless.key.protection.UnlockSecretKey; +import org.pgpainless.key.util.KeyIdUtil; import org.pgpainless.policy.Policy; import org.pgpainless.signature.SignatureUtils; import org.pgpainless.signature.consumer.OnePassSignatureCheck; @@ -234,6 +235,7 @@ public class OpenPgpMessageInputStream extends DecryptionStream { // Marker Packets need to be skipped and ignored case MARKER: + LOGGER.debug("Skipping Marker Packet"); packetInputStream.readMarker(); break; @@ -263,6 +265,7 @@ public class OpenPgpMessageInputStream extends DecryptionStream { } private void processLiteralData() throws IOException { + LOGGER.debug("Literal Data Packet at depth " + metadata.depth + " encountered"); automaton.next(InputAlphabet.LiteralData); PGPLiteralData literalData = packetInputStream.readLiteralData(); this.metadata.setChild(new MessageMetadata.LiteralData( @@ -279,6 +282,7 @@ public class OpenPgpMessageInputStream extends DecryptionStream { MessageMetadata.CompressedData compressionLayer = new MessageMetadata.CompressedData( CompressionAlgorithm.fromId(compressedData.getAlgorithm()), metadata.depth + 1); + LOGGER.debug("Compressed Data Packet (" + compressionLayer.algorithm + ") at depth " + metadata.depth + " encountered"); InputStream decompressed = compressedData.getDataStream(); nestedInputStream = new OpenPgpMessageInputStream(buffer(decompressed), options, compressionLayer, policy); } @@ -286,6 +290,8 @@ public class OpenPgpMessageInputStream extends DecryptionStream { private void processOnePassSignature() throws PGPException, IOException { automaton.next(InputAlphabet.OnePassSignature); PGPOnePassSignature onePassSignature = packetInputStream.readOnePassSignature(); + LOGGER.debug("One-Pass-Signature Packet by key " + KeyIdUtil.formatKeyId(onePassSignature.getKeyID()) + + "at depth " + metadata.depth + " encountered"); signatures.addOnePassSignature(onePassSignature); } @@ -294,42 +300,59 @@ public class OpenPgpMessageInputStream extends DecryptionStream { boolean isSigForOPS = automaton.peekStack() == StackAlphabet.ops; automaton.next(InputAlphabet.Signature); PGPSignature signature = packetInputStream.readSignature(); + long keyId = SignatureUtils.determineIssuerKeyId(signature); if (isSigForOPS) { + LOGGER.debug("Signature Packet corresponding to One-Pass-Signature by key " + + KeyIdUtil.formatKeyId(keyId) + + " at depth " + metadata.depth + " encountered"); signatures.leaveNesting(); // TODO: Only leave nesting if all OPSs of the nesting layer are dealt with signatures.addCorrespondingOnePassSignature(signature, metadata, policy); } else { + LOGGER.debug("Prepended Signature Packet by key " + + KeyIdUtil.formatKeyId(keyId) + + " at depth " + metadata.depth + " encountered"); signatures.addPrependedSignature(signature); } } private boolean processEncryptedData() throws IOException, PGPException { + LOGGER.debug("Symmetrically Encrypted Data Packet at depth " + metadata.depth + " encountered"); automaton.next(InputAlphabet.EncryptedData); PGPEncryptedDataList encDataList = packetInputStream.readEncryptedDataList(); // TODO: Replace with !encDataList.isIntegrityProtected() // once BC ships it if (!encDataList.get(0).isIntegrityProtected()) { + LOGGER.debug("Symmetrically Encrypted Data Packet is not integrity-protected and is therefore rejected."); throw new MessageNotIntegrityProtectedException(); } SortedESKs esks = new SortedESKs(encDataList); + LOGGER.debug("Symmetrically Encrypted Integrity-Protected Data has " + + esks.skesks.size() + " SKESK(s) and " + + (esks.pkesks.size() + esks.anonPkesks.size()) + " PKESK(s) from which " + + esks.anonPkesks.size() + " PKESK(s) have an anonymous recipient"); - // Try custom decryptor factory + // Try custom decryptor factories for (SubkeyIdentifier subkeyIdentifier : options.getCustomDecryptorFactories().keySet()) { + LOGGER.debug("Attempt decryption with custom decryptor factory with key " + subkeyIdentifier); PublicKeyDataDecryptorFactory decryptorFactory = options.getCustomDecryptorFactories().get(subkeyIdentifier); for (PGPPublicKeyEncryptedData pkesk : esks.pkesks) { + // find matching PKESK if (pkesk.getKeyID() != subkeyIdentifier.getSubkeyId()) { continue; } + // attempt decryption if (decryptPKESKAndStream(subkeyIdentifier, decryptorFactory, pkesk)) { return true; } } } - // Try session key + // Try provided session key if (options.getSessionKey() != null) { + LOGGER.debug("Attempt decryption with provided session key"); SessionKey sessionKey = options.getSessionKey(); throwIfUnacceptable(sessionKey.getAlgorithm()); @@ -340,6 +363,7 @@ public class OpenPgpMessageInputStream extends DecryptionStream { try { // TODO: Use BCs new API once shipped + // see https://github.com/bcgit/bc-java/pull/1228 (discussion) PGPEncryptedData esk = esks.all().get(0); if (esk instanceof PGPPBEEncryptedData) { PGPPBEEncryptedData skesk = (PGPPBEEncryptedData) esk; @@ -347,6 +371,7 @@ public class OpenPgpMessageInputStream extends DecryptionStream { encryptedData.sessionKey = sessionKey; IntegrityProtectedInputStream integrityProtected = new IntegrityProtectedInputStream(decrypted, skesk, options); nestedInputStream = new OpenPgpMessageInputStream(buffer(integrityProtected), options, encryptedData, policy); + LOGGER.debug("Successfully decrypted data with provided session key"); return true; } else if (esk instanceof PGPPublicKeyEncryptedData) { PGPPublicKeyEncryptedData pkesk = (PGPPublicKeyEncryptedData) esk; @@ -354,20 +379,29 @@ public class OpenPgpMessageInputStream extends DecryptionStream { encryptedData.sessionKey = sessionKey; IntegrityProtectedInputStream integrityProtected = new IntegrityProtectedInputStream(decrypted, pkesk, options); nestedInputStream = new OpenPgpMessageInputStream(buffer(integrityProtected), options, encryptedData, policy); + LOGGER.debug("Successfully decrypted data with provided session key"); return true; } else { throw new RuntimeException("Unknown ESK class type: " + esk.getClass().getName()); } } catch (PGPException e) { // Session key mismatch? + LOGGER.debug("Decryption using provided session key failed. Mismatched session key and message?", e); } } // Try passwords - for (PGPPBEEncryptedData skesk : esks.skesks) { - SymmetricKeyAlgorithm kekAlgorithm = SymmetricKeyAlgorithm.requireFromId(skesk.getAlgorithm()); - throwIfUnacceptable(kekAlgorithm); - for (Passphrase passphrase : options.getDecryptionPassphrases()) { + for (Passphrase passphrase : options.getDecryptionPassphrases()) { + for (PGPPBEEncryptedData skesk : esks.skesks) { + LOGGER.debug("Attempt decryption with provided passphrase"); + SymmetricKeyAlgorithm kekAlgorithm = SymmetricKeyAlgorithm.requireFromId(skesk.getAlgorithm()); + try { + throwIfUnacceptable(kekAlgorithm); + } catch (UnacceptableAlgorithmException e) { + LOGGER.debug("Skipping SKESK with unacceptable encapsulation algorithm", e); + continue; + } + PBEDataDecryptorFactory decryptorFactory = ImplementationFactory.getInstance() .getPBEDataDecryptorFactory(passphrase); @@ -381,22 +415,25 @@ public class OpenPgpMessageInputStream extends DecryptionStream { // Try (known) secret keys for (PGPPublicKeyEncryptedData pkesk : esks.pkesks) { long keyId = pkesk.getKeyID(); + LOGGER.debug("Encountered PKESK with recipient " + KeyIdUtil.formatKeyId(keyId)); PGPSecretKeyRing decryptionKeys = getDecryptionKey(keyId); if (decryptionKeys == null) { + LOGGER.debug("Skipping PKESK because no matching key " + KeyIdUtil.formatKeyId(keyId) + " was provided"); continue; } PGPSecretKey secretKey = decryptionKeys.getSecretKey(keyId); + SubkeyIdentifier decryptionKeyId = new SubkeyIdentifier(decryptionKeys, secretKey.getKeyID()); + LOGGER.debug("Attempt decryption using secret key " + decryptionKeyId); SecretKeyRingProtector protector = options.getSecretKeyProtector(decryptionKeys); // Postpone keys with missing passphrase if (!protector.hasPassphraseFor(keyId)) { + LOGGER.debug("Missing passphrase for key " + decryptionKeyId + ". Postponing decryption until all other keys were tried"); postponedDueToMissingPassphrase.add(new Tuple<>(secretKey, pkesk)); continue; } - PGPSecretKey decryptionKey = decryptionKeys.getSecretKey(keyId); - SubkeyIdentifier decryptionKeyId = new SubkeyIdentifier(decryptionKeys, decryptionKey.getKeyID()); - PGPPrivateKey privateKey = UnlockSecretKey.unlockSecretKey(decryptionKey, protector); + PGPPrivateKey privateKey = UnlockSecretKey.unlockSecretKey(secretKey, protector); PublicKeyDataDecryptorFactory decryptorFactory = ImplementationFactory.getInstance() .getPublicKeyDataDecryptorFactory(privateKey); @@ -408,16 +445,19 @@ public class OpenPgpMessageInputStream extends DecryptionStream { // try anonymous secret keys for (PGPPublicKeyEncryptedData pkesk : esks.anonPkesks) { for (Tuple decryptionKeyCandidate : findPotentialDecryptionKeys(pkesk)) { + PGPSecretKeyRing decryptionKeys = decryptionKeyCandidate.getA(); PGPSecretKey secretKey = decryptionKeyCandidate.getB(); + SubkeyIdentifier decryptionKeyId = new SubkeyIdentifier(decryptionKeys, secretKey.getKeyID()); + LOGGER.debug("Attempt decryption of anonymous PKESK with key " + decryptionKeyId); SecretKeyRingProtector protector = options.getSecretKeyProtector(decryptionKeyCandidate.getA()); if (!protector.hasPassphraseFor(secretKey.getKeyID())) { + LOGGER.debug("Missing passphrase for key " + decryptionKeyId + ". Postponing decryption until all other keys were tried."); postponedDueToMissingPassphrase.add(new Tuple<>(secretKey, pkesk)); continue; } PGPPrivateKey privateKey = UnlockSecretKey.unlockSecretKey(decryptionKeyCandidate.getB(), protector); PublicKeyDataDecryptorFactory decryptorFactory = ImplementationFactory.getInstance() .getPublicKeyDataDecryptorFactory(privateKey); - SubkeyIdentifier decryptionKeyId = new SubkeyIdentifier(decryptionKeyCandidate.getA(), privateKey.getKeyID()); if (decryptPKESKAndStream(decryptionKeyId, decryptorFactory, pkesk)) { return true; @@ -442,9 +482,10 @@ public class OpenPgpMessageInputStream extends DecryptionStream { PGPSecretKey secretKey = missingPassphrases.getA(); long keyId = secretKey.getKeyID(); PGPSecretKeyRing decryptionKey = getDecryptionKey(keyId); + SubkeyIdentifier decryptionKeyId = new SubkeyIdentifier(decryptionKey, keyId); + LOGGER.debug("Attempt decryption with key " + decryptionKeyId + " while interactively requesting its passphrase"); SecretKeyRingProtector protector = options.getSecretKeyProtector(decryptionKey); PGPPrivateKey privateKey = UnlockSecretKey.unlockSecretKey(secretKey, protector.getDecryptor(keyId)); - SubkeyIdentifier decryptionKeyId = new SubkeyIdentifier(decryptionKey, keyId); PublicKeyDataDecryptorFactory decryptorFactory = ImplementationFactory.getInstance() .getPublicKeyDataDecryptorFactory(privateKey); @@ -458,10 +499,13 @@ public class OpenPgpMessageInputStream extends DecryptionStream { } // we did not yet succeed in decrypting any session key :/ + + LOGGER.debug("Failed to decrypt encrypted data packet"); return false; } - private boolean decryptSKESKAndStream(PGPPBEEncryptedData skesk, PBEDataDecryptorFactory decryptorFactory) throws IOException, UnacceptableAlgorithmException { + private boolean decryptSKESKAndStream(PGPPBEEncryptedData skesk, PBEDataDecryptorFactory decryptorFactory) + throws IOException, UnacceptableAlgorithmException { try { InputStream decrypted = skesk.getDataStream(decryptorFactory); SessionKey sessionKey = new SessionKey(skesk.getSessionKey(decryptorFactory)); @@ -469,18 +513,19 @@ public class OpenPgpMessageInputStream extends DecryptionStream { MessageMetadata.EncryptedData encryptedData = new MessageMetadata.EncryptedData( sessionKey.getAlgorithm(), metadata.depth + 1); encryptedData.sessionKey = sessionKey; + LOGGER.debug("Successfully decrypted data with passphrase"); IntegrityProtectedInputStream integrityProtected = new IntegrityProtectedInputStream(decrypted, skesk, options); nestedInputStream = new OpenPgpMessageInputStream(buffer(integrityProtected), options, encryptedData, policy); return true; } catch (UnacceptableAlgorithmException e) { throw e; } catch (PGPException e) { - // Password mismatch? + LOGGER.debug("Decryption of encrypted data packet using password failed. Password mismatch?", e); } return false; } - private boolean decryptPKESKAndStream(SubkeyIdentifier subkeyIdentifier, + private boolean decryptPKESKAndStream(SubkeyIdentifier decryptionKeyId, PublicKeyDataDecryptorFactory decryptorFactory, PGPPublicKeyEncryptedData pkesk) throws IOException, UnacceptableAlgorithmException { @@ -492,16 +537,17 @@ public class OpenPgpMessageInputStream extends DecryptionStream { MessageMetadata.EncryptedData encryptedData = new MessageMetadata.EncryptedData( SymmetricKeyAlgorithm.requireFromId(pkesk.getSymmetricAlgorithm(decryptorFactory)), metadata.depth + 1); - encryptedData.decryptionKey = subkeyIdentifier; + encryptedData.decryptionKey = decryptionKeyId; encryptedData.sessionKey = sessionKey; + LOGGER.debug("Successfully decrypted data with key " + decryptionKeyId); IntegrityProtectedInputStream integrityProtected = new IntegrityProtectedInputStream(decrypted, pkesk, options); nestedInputStream = new OpenPgpMessageInputStream(buffer(integrityProtected), options, encryptedData, policy); return true; } catch (UnacceptableAlgorithmException e) { throw e; } catch (PGPException e) { - + LOGGER.debug("Decryption of encrypted data packet using secret key failed.", e); } return false; } @@ -823,8 +869,10 @@ public class OpenPgpMessageInputStream extends DecryptionStream { SignatureValidator.signatureWasCreatedInBounds(options.getVerifyNotBefore(), options.getVerifyNotAfter()) .verify(signature); SignatureVerifier.verifyOnePassSignature(signature, onePassSignature.getVerificationKeys().getPublicKey(signature.getKeyID()), onePassSignature, policy); + LOGGER.debug("Acceptable signature by key " + verification.getSigningKey()); layer.addVerifiedOnePassSignature(verification); } catch (SignatureValidationException e) { + LOGGER.debug("Rejected signature by key " + verification.getSigningKey(), e); layer.addRejectedOnePassSignature(new SignatureVerification.Failure(verification, e)); } break; @@ -929,8 +977,10 @@ public class OpenPgpMessageInputStream extends DecryptionStream { detached.getSignature(), detached.getSigningKeyRing().getPublicKey(detached.getSigningKeyIdentifier().getKeyId()), policy, detached.getSignature().getCreationTime()); + LOGGER.debug("Acceptable signature by key " + verification.getSigningKey()); layer.addVerifiedDetachedSignature(verification); } catch (SignatureValidationException e) { + LOGGER.debug("Rejected signature by key " + verification.getSigningKey(), e); layer.addRejectedDetachedSignature(new SignatureVerification.Failure(verification, e)); } } @@ -944,8 +994,10 @@ public class OpenPgpMessageInputStream extends DecryptionStream { prepended.getSignature(), prepended.getSigningKeyRing().getPublicKey(prepended.getSigningKeyIdentifier().getKeyId()), policy, prepended.getSignature().getCreationTime()); + LOGGER.debug("Acceptable signature by key " + verification.getSigningKey()); layer.addVerifiedPrependedSignature(verification); } catch (SignatureValidationException e) { + LOGGER.debug("Rejected signature by key " + verification.getSigningKey(), e); layer.addRejectedPrependedSignature(new SignatureVerification.Failure(verification, e)); } } diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/automaton/PDA.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/automaton/PDA.java index 156dc2ed..3df3c5b5 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/automaton/PDA.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/automaton/PDA.java @@ -5,6 +5,8 @@ package org.pgpainless.decryption_verification.automaton; import org.pgpainless.exception.MalformedOpenPgpMessageException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import java.util.Stack; @@ -15,11 +17,11 @@ import static org.pgpainless.decryption_verification.automaton.StackAlphabet.ter public class PDA { private static int ID = 0; + private static final Logger LOGGER = LoggerFactory.getLogger(PDA.class); /** * Set of states of the automaton. - * Each state defines its valid transitions in their {@link NestingPDA.State#transition(InputAlphabet, NestingPDA)} - * method. + * Each state defines its valid transitions in their {@link State#transition(InputAlphabet, PDA)} method. */ public enum State { @@ -199,7 +201,12 @@ public class PDA { } public void next(InputAlphabet input) throws MalformedOpenPgpMessageException { - state = state.transition(input, this); + try { + state = state.transition(input, this); + } catch (MalformedOpenPgpMessageException e) { + LOGGER.debug("Unexpected Packet or Token '" + input + "' encountered. Message is malformed.", e); + throw e; + } } /** diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/util/KeyIdUtil.java b/pgpainless-core/src/main/java/org/pgpainless/key/util/KeyIdUtil.java index e6ab4f48..7ebac8fd 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/key/util/KeyIdUtil.java +++ b/pgpainless-core/src/main/java/org/pgpainless/key/util/KeyIdUtil.java @@ -30,4 +30,8 @@ public final class KeyIdUtil { return new BigInteger(longKeyId, 16).longValue(); } + + public static String formatKeyId(long keyId) { + return Long.toHexString(keyId).toUpperCase(); + } } From 977f8c4101fdf5e2cd42049d1c2ac9f116a7b108 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Fri, 21 Oct 2022 00:31:25 +0200 Subject: [PATCH 057/763] Rename automaton package to syntax_check --- .../OpenPgpMessageInputStream.java | 33 ++++++++++--------- .../InputAlphabet.java | 2 +- .../{automaton => syntax_check}/PDA.java | 8 ++--- .../StackAlphabet.java | 2 +- .../package-info.java | 2 +- .../MalformedOpenPgpMessageException.java | 6 ++-- .../{automaton => syntax_check}/PDATest.java | 2 +- 7 files changed, 28 insertions(+), 27 deletions(-) rename pgpainless-core/src/main/java/org/pgpainless/decryption_verification/{automaton => syntax_check}/InputAlphabet.java (95%) rename pgpainless-core/src/main/java/org/pgpainless/decryption_verification/{automaton => syntax_check}/PDA.java (96%) rename pgpainless-core/src/main/java/org/pgpainless/decryption_verification/{automaton => syntax_check}/StackAlphabet.java (89%) rename pgpainless-core/src/main/java/org/pgpainless/decryption_verification/{automaton => syntax_check}/package-info.java (78%) rename pgpainless-core/src/test/java/org/pgpainless/decryption_verification/{automaton => syntax_check}/PDATest.java (97%) diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java index 1dbadbbe..a2fa2351 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java @@ -43,9 +43,9 @@ import org.pgpainless.algorithm.EncryptionPurpose; import org.pgpainless.algorithm.OpenPgpPacket; import org.pgpainless.algorithm.StreamEncoding; import org.pgpainless.algorithm.SymmetricKeyAlgorithm; -import org.pgpainless.decryption_verification.automaton.InputAlphabet; -import org.pgpainless.decryption_verification.automaton.PDA; -import org.pgpainless.decryption_verification.automaton.StackAlphabet; +import org.pgpainless.decryption_verification.syntax_check.InputAlphabet; +import org.pgpainless.decryption_verification.syntax_check.PDA; +import org.pgpainless.decryption_verification.syntax_check.StackAlphabet; import org.pgpainless.decryption_verification.cleartext_signatures.ClearsignedMessageUtil; import org.pgpainless.decryption_verification.cleartext_signatures.MultiPassStrategy; import org.pgpainless.exception.MalformedOpenPgpMessageException; @@ -80,7 +80,7 @@ public class OpenPgpMessageInputStream extends DecryptionStream { // Options to consume the data protected final ConsumerOptions options; // Pushdown Automaton to verify validity of OpenPGP packet sequence in an OpenPGP message - protected final PDA automaton = new PDA(); + protected final PDA syntaxVerifier = new PDA(); // InputStream of OpenPGP packets protected TeeBCPGInputStream packetInputStream; // InputStream of a nested data packet @@ -266,7 +266,7 @@ public class OpenPgpMessageInputStream extends DecryptionStream { private void processLiteralData() throws IOException { LOGGER.debug("Literal Data Packet at depth " + metadata.depth + " encountered"); - automaton.next(InputAlphabet.LiteralData); + syntaxVerifier.next(InputAlphabet.LiteralData); PGPLiteralData literalData = packetInputStream.readLiteralData(); this.metadata.setChild(new MessageMetadata.LiteralData( literalData.getFileName(), @@ -276,7 +276,7 @@ public class OpenPgpMessageInputStream extends DecryptionStream { } private void processCompressedData() throws IOException, PGPException { - automaton.next(InputAlphabet.CompressedData); + syntaxVerifier.next(InputAlphabet.CompressedData); signatures.enterNesting(); PGPCompressedData compressedData = packetInputStream.readCompressedData(); MessageMetadata.CompressedData compressionLayer = new MessageMetadata.CompressedData( @@ -288,7 +288,7 @@ public class OpenPgpMessageInputStream extends DecryptionStream { } private void processOnePassSignature() throws PGPException, IOException { - automaton.next(InputAlphabet.OnePassSignature); + syntaxVerifier.next(InputAlphabet.OnePassSignature); PGPOnePassSignature onePassSignature = packetInputStream.readOnePassSignature(); LOGGER.debug("One-Pass-Signature Packet by key " + KeyIdUtil.formatKeyId(onePassSignature.getKeyID()) + "at depth " + metadata.depth + " encountered"); @@ -297,8 +297,8 @@ public class OpenPgpMessageInputStream extends DecryptionStream { private void processSignature() throws PGPException, IOException { // true if Signature corresponds to OnePassSignature - boolean isSigForOPS = automaton.peekStack() == StackAlphabet.ops; - automaton.next(InputAlphabet.Signature); + boolean isSigForOPS = syntaxVerifier.peekStack() == StackAlphabet.ops; + syntaxVerifier.next(InputAlphabet.Signature); PGPSignature signature = packetInputStream.readSignature(); long keyId = SignatureUtils.determineIssuerKeyId(signature); if (isSigForOPS) { @@ -317,7 +317,7 @@ public class OpenPgpMessageInputStream extends DecryptionStream { private boolean processEncryptedData() throws IOException, PGPException { LOGGER.debug("Symmetrically Encrypted Data Packet at depth " + metadata.depth + " encountered"); - automaton.next(InputAlphabet.EncryptedData); + syntaxVerifier.next(InputAlphabet.EncryptedData); PGPEncryptedDataList encDataList = packetInputStream.readEncryptedDataList(); // TODO: Replace with !encDataList.isIntegrityProtected() @@ -601,7 +601,7 @@ public class OpenPgpMessageInputStream extends DecryptionStream { @Override public int read() throws IOException { if (nestedInputStream == null) { - automaton.assertValid(); + syntaxVerifier.assertValid(); return -1; } @@ -638,7 +638,7 @@ public class OpenPgpMessageInputStream extends DecryptionStream { if (nestedInputStream == null) { if (packetInputStream != null) { - automaton.assertValid(); + syntaxVerifier.assertValid(); } return -1; } @@ -668,7 +668,7 @@ public class OpenPgpMessageInputStream extends DecryptionStream { super.close(); if (closed) { if (packetInputStream != null) { - automaton.assertValid(); + syntaxVerifier.assertValid(); } return; } @@ -686,8 +686,8 @@ public class OpenPgpMessageInputStream extends DecryptionStream { } if (packetInputStream != null) { - automaton.next(InputAlphabet.EndOfSequence); - automaton.assertValid(); + syntaxVerifier.next(InputAlphabet.EndOfSequence); + syntaxVerifier.assertValid(); packetInputStream.close(); } closed = true; @@ -717,7 +717,8 @@ public class OpenPgpMessageInputStream extends DecryptionStream { for (PGPEncryptedData esk : esks) { if (esk instanceof PGPPBEEncryptedData) { skesks.add((PGPPBEEncryptedData) esk); - } else if (esk instanceof PGPPublicKeyEncryptedData) { + } + else if (esk instanceof PGPPublicKeyEncryptedData) { PGPPublicKeyEncryptedData pkesk = (PGPPublicKeyEncryptedData) esk; if (pkesk.getKeyID() != 0) { pkesks.add(pkesk); diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/automaton/InputAlphabet.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/InputAlphabet.java similarity index 95% rename from pgpainless-core/src/main/java/org/pgpainless/decryption_verification/automaton/InputAlphabet.java rename to pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/InputAlphabet.java index ad2a8c55..f73ede34 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/automaton/InputAlphabet.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/InputAlphabet.java @@ -2,7 +2,7 @@ // // SPDX-License-Identifier: Apache-2.0 -package org.pgpainless.decryption_verification.automaton; +package org.pgpainless.decryption_verification.syntax_check; import org.bouncycastle.openpgp.PGPCompressedData; import org.bouncycastle.openpgp.PGPEncryptedDataList; diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/automaton/PDA.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/PDA.java similarity index 96% rename from pgpainless-core/src/main/java/org/pgpainless/decryption_verification/automaton/PDA.java rename to pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/PDA.java index 3df3c5b5..0d6ba28c 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/automaton/PDA.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/PDA.java @@ -2,7 +2,7 @@ // // SPDX-License-Identifier: Apache-2.0 -package org.pgpainless.decryption_verification.automaton; +package org.pgpainless.decryption_verification.syntax_check; import org.pgpainless.exception.MalformedOpenPgpMessageException; import org.slf4j.Logger; @@ -10,9 +10,9 @@ import org.slf4j.LoggerFactory; import java.util.Stack; -import static org.pgpainless.decryption_verification.automaton.StackAlphabet.msg; -import static org.pgpainless.decryption_verification.automaton.StackAlphabet.ops; -import static org.pgpainless.decryption_verification.automaton.StackAlphabet.terminus; +import static org.pgpainless.decryption_verification.syntax_check.StackAlphabet.msg; +import static org.pgpainless.decryption_verification.syntax_check.StackAlphabet.ops; +import static org.pgpainless.decryption_verification.syntax_check.StackAlphabet.terminus; public class PDA { diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/automaton/StackAlphabet.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/StackAlphabet.java similarity index 89% rename from pgpainless-core/src/main/java/org/pgpainless/decryption_verification/automaton/StackAlphabet.java rename to pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/StackAlphabet.java index 09865f31..6030fbc8 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/automaton/StackAlphabet.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/StackAlphabet.java @@ -2,7 +2,7 @@ // // SPDX-License-Identifier: Apache-2.0 -package org.pgpainless.decryption_verification.automaton; +package org.pgpainless.decryption_verification.syntax_check; public enum StackAlphabet { /** diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/automaton/package-info.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/package-info.java similarity index 78% rename from pgpainless-core/src/main/java/org/pgpainless/decryption_verification/automaton/package-info.java rename to pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/package-info.java index 80a79e85..4df6af5a 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/automaton/package-info.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/package-info.java @@ -5,4 +5,4 @@ /** * Pushdown Automaton to verify validity of packet sequences according to the OpenPGP Message format. */ -package org.pgpainless.decryption_verification.automaton; +package org.pgpainless.decryption_verification.syntax_check; diff --git a/pgpainless-core/src/main/java/org/pgpainless/exception/MalformedOpenPgpMessageException.java b/pgpainless-core/src/main/java/org/pgpainless/exception/MalformedOpenPgpMessageException.java index 0069209c..cbe78c05 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/exception/MalformedOpenPgpMessageException.java +++ b/pgpainless-core/src/main/java/org/pgpainless/exception/MalformedOpenPgpMessageException.java @@ -4,9 +4,9 @@ package org.pgpainless.exception; -import org.pgpainless.decryption_verification.automaton.InputAlphabet; -import org.pgpainless.decryption_verification.automaton.PDA; -import org.pgpainless.decryption_verification.automaton.StackAlphabet; +import org.pgpainless.decryption_verification.syntax_check.InputAlphabet; +import org.pgpainless.decryption_verification.syntax_check.PDA; +import org.pgpainless.decryption_verification.syntax_check.StackAlphabet; /** * Exception that gets thrown if the OpenPGP message is malformed. diff --git a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/automaton/PDATest.java b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/syntax_check/PDATest.java similarity index 97% rename from pgpainless-core/src/test/java/org/pgpainless/decryption_verification/automaton/PDATest.java rename to pgpainless-core/src/test/java/org/pgpainless/decryption_verification/syntax_check/PDATest.java index 6dbb2cd6..346e1f3b 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/automaton/PDATest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/syntax_check/PDATest.java @@ -1,4 +1,4 @@ -package org.pgpainless.decryption_verification.automaton; +package org.pgpainless.decryption_verification.syntax_check; import org.junit.jupiter.api.Test; import org.pgpainless.exception.MalformedOpenPgpMessageException; From 3977d1f40785ef71fdafa9b41d6a3bd3cdc2a899 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Fri, 21 Oct 2022 00:40:40 +0200 Subject: [PATCH 058/763] Add more direct PDA tests --- .../syntax_check/PDATest.java | 86 ++++++++++++++----- 1 file changed, 63 insertions(+), 23 deletions(-) diff --git a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/syntax_check/PDATest.java b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/syntax_check/PDATest.java index 346e1f3b..e4877d94 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/syntax_check/PDATest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/syntax_check/PDATest.java @@ -1,10 +1,11 @@ package org.pgpainless.decryption_verification.syntax_check; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + import org.junit.jupiter.api.Test; import org.pgpainless.exception.MalformedOpenPgpMessageException; -import static org.junit.jupiter.api.Assertions.assertTrue; - public class PDATest { @@ -15,11 +16,11 @@ public class PDATest { */ @Test public void testSimpleLiteralMessageIsValid() throws MalformedOpenPgpMessageException { - PDA automaton = new PDA(); - automaton.next(InputAlphabet.LiteralData); - automaton.next(InputAlphabet.EndOfSequence); + PDA check = new PDA(); + check.next(InputAlphabet.LiteralData); + check.next(InputAlphabet.EndOfSequence); - assertTrue(automaton.isValid()); + assertTrue(check.isValid()); } /** @@ -29,13 +30,13 @@ public class PDATest { */ @Test public void testSimpleOpsSignedMesssageIsValid() throws MalformedOpenPgpMessageException { - PDA automaton = new PDA(); - automaton.next(InputAlphabet.OnePassSignature); - automaton.next(InputAlphabet.LiteralData); - automaton.next(InputAlphabet.Signature); - automaton.next(InputAlphabet.EndOfSequence); + PDA check = new PDA(); + check.next(InputAlphabet.OnePassSignature); + check.next(InputAlphabet.LiteralData); + check.next(InputAlphabet.Signature); + check.next(InputAlphabet.EndOfSequence); - assertTrue(automaton.isValid()); + assertTrue(check.isValid()); } @@ -46,12 +47,12 @@ public class PDATest { */ @Test public void testSimplePrependSignedMessageIsValid() throws MalformedOpenPgpMessageException { - PDA automaton = new PDA(); - automaton.next(InputAlphabet.Signature); - automaton.next(InputAlphabet.LiteralData); - automaton.next(InputAlphabet.EndOfSequence); + PDA check = new PDA(); + check.next(InputAlphabet.Signature); + check.next(InputAlphabet.LiteralData); + check.next(InputAlphabet.EndOfSequence); - assertTrue(automaton.isValid()); + assertTrue(check.isValid()); } @@ -62,14 +63,53 @@ public class PDATest { */ @Test public void testOPSSignedCompressedMessageIsValid() throws MalformedOpenPgpMessageException { - PDA automaton = new PDA(); - automaton.next(InputAlphabet.OnePassSignature); - automaton.next(InputAlphabet.CompressedData); + PDA check = new PDA(); + check.next(InputAlphabet.OnePassSignature); + check.next(InputAlphabet.CompressedData); // Here would be a nested PDA for the LiteralData packet - automaton.next(InputAlphabet.Signature); - automaton.next(InputAlphabet.EndOfSequence); + check.next(InputAlphabet.Signature); + check.next(InputAlphabet.EndOfSequence); - assertTrue(automaton.isValid()); + assertTrue(check.isValid()); } + @Test + public void testTwoLiteralDataIsNotValid() { + PDA check = new PDA(); + check.next(InputAlphabet.LiteralData); + assertThrows(MalformedOpenPgpMessageException.class, + () -> check.next(InputAlphabet.LiteralData)); + } + + @Test + public void testTrailingSigIsNotValid() { + PDA check = new PDA(); + check.next(InputAlphabet.LiteralData); + assertThrows(MalformedOpenPgpMessageException.class, + () -> check.next(InputAlphabet.Signature)); + } + + @Test + public void testOPSWithPrependedSigIsValid() { + PDA check = new PDA(); + check.next(InputAlphabet.Signature); + check.next(InputAlphabet.OnePassSignature); + check.next(InputAlphabet.LiteralData); + check.next(InputAlphabet.Signature); + check.next(InputAlphabet.EndOfSequence); + + assertTrue(check.isValid()); + } + + @Test + public void testPrependedSigInsideOPSSignedMessageIsValid() { + PDA check = new PDA(); + check.next(InputAlphabet.OnePassSignature); + check.next(InputAlphabet.Signature); + check.next(InputAlphabet.LiteralData); + check.next(InputAlphabet.Signature); + check.next(InputAlphabet.EndOfSequence); + + assertTrue(check.isValid()); + } } From 54cb9dad713fb466ce234490838f1e670c75f0b6 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Fri, 21 Oct 2022 13:46:33 +0200 Subject: [PATCH 059/763] Further increase coverage of PDA class --- .../syntax_check/PDATest.java | 78 +++++++++++++++++++ 1 file changed, 78 insertions(+) diff --git a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/syntax_check/PDATest.java b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/syntax_check/PDATest.java index e4877d94..8fa107f6 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/syntax_check/PDATest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/syntax_check/PDATest.java @@ -73,6 +73,42 @@ public class PDATest { assertTrue(check.isValid()); } + @Test + public void testOPSSignedEncryptedMessageIsValid() { + PDA check = new PDA(); + check.next(InputAlphabet.OnePassSignature); + check.next(InputAlphabet.EncryptedData); + check.next(InputAlphabet.Signature); + check.next(InputAlphabet.EndOfSequence); + assertTrue(check.isValid()); + } + + @Test + public void anyInputAfterEOSIsNotValid() { + PDA check = new PDA(); + check.next(InputAlphabet.LiteralData); + check.next(InputAlphabet.EndOfSequence); + assertThrows(MalformedOpenPgpMessageException.class, + () -> check.next(InputAlphabet.Signature)); + } + + @Test + public void testEncryptedMessageWithAppendedStandalongSigIsNotValid() { + PDA check = new PDA(); + check.next(InputAlphabet.EncryptedData); + assertThrows(MalformedOpenPgpMessageException.class, + () -> check.next(InputAlphabet.Signature)); + } + + @Test + public void testOPSSignedEncryptedMessageWithMissingSigIsNotValid() { + PDA check = new PDA(); + check.next(InputAlphabet.OnePassSignature); + check.next(InputAlphabet.EncryptedData); + assertThrows(MalformedOpenPgpMessageException.class, + () -> check.next(InputAlphabet.EndOfSequence)); + } + @Test public void testTwoLiteralDataIsNotValid() { PDA check = new PDA(); @@ -89,6 +125,48 @@ public class PDATest { () -> check.next(InputAlphabet.Signature)); } + @Test + public void testOPSAloneIsNotValid() { + PDA check = new PDA(); + check.next(InputAlphabet.OnePassSignature); + assertThrows(MalformedOpenPgpMessageException.class, + () -> check.next(InputAlphabet.EndOfSequence)); + } + + @Test + public void testOPSLitWithMissingSigIsNotValid() { + PDA check = new PDA(); + check.next(InputAlphabet.OnePassSignature); + check.next(InputAlphabet.LiteralData); + assertThrows(MalformedOpenPgpMessageException.class, + () -> check.next(InputAlphabet.EndOfSequence)); + } + + @Test + public void testCompressedMessageWithStandalongAppendedSigIsNotValid() { + PDA check = new PDA(); + check.next(InputAlphabet.CompressedData); + assertThrows(MalformedOpenPgpMessageException.class, + () -> check.next(InputAlphabet.Signature)); + } + + @Test + public void testOPSCompressedDataWithMissingSigIsNotValid() { + PDA check = new PDA(); + check.next(InputAlphabet.OnePassSignature); + check.next(InputAlphabet.CompressedData); + assertThrows(MalformedOpenPgpMessageException.class, + () -> check.next(InputAlphabet.EndOfSequence)); + } + + @Test + public void testCompressedMessageFollowedByTrailingLiteralDataIsNotValid() { + PDA check = new PDA(); + check.next(InputAlphabet.CompressedData); + assertThrows(MalformedOpenPgpMessageException.class, + () -> check.next(InputAlphabet.LiteralData)); + } + @Test public void testOPSWithPrependedSigIsValid() { PDA check = new PDA(); From 3f8653cf2ea831d9d68bc40f6d1b72cd510d8531 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Fri, 21 Oct 2022 16:16:45 +0200 Subject: [PATCH 060/763] Fix CRCing test and fully depend on new stream for decryption --- .../DecryptionBuilder.java | 2 +- .../MessageMetadata.java | 22 ++- .../OpenPgpMessageInputStream.java | 152 +++++++++++++----- .../TeeBCPGInputStream.java | 14 +- .../OpenPgpMessageInputStreamTest.java | 2 +- 5 files changed, 137 insertions(+), 55 deletions(-) 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 68a68847..0baf4124 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 @@ -31,7 +31,7 @@ public class DecryptionBuilder implements DecryptionBuilderInterface { throw new IllegalArgumentException("Consumer options cannot be null."); } - return DecryptionStreamFactory.create(inputStream, consumerOptions); + return OpenPgpMessageInputStream.create(inputStream, consumerOptions); } } } diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/MessageMetadata.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/MessageMetadata.java index 59f8052a..7811578e 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/MessageMetadata.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/MessageMetadata.java @@ -164,19 +164,35 @@ public class MessageMetadata { } public String getFilename() { - return findLiteralData().getFileName(); + LiteralData literalData = findLiteralData(); + if (literalData == null) { + return null; + } + return literalData.getFileName(); } public Date getModificationDate() { - return findLiteralData().getModificationDate(); + LiteralData literalData = findLiteralData(); + if (literalData == null) { + return null; + } + return literalData.getModificationDate(); } public StreamEncoding getFormat() { - return findLiteralData().getFormat(); + LiteralData literalData = findLiteralData(); + if (literalData == null) { + return null; + } + return literalData.getFormat(); } private LiteralData findLiteralData() { Nested nested = message.child; + if (nested == null) { + return null; + } + while (nested.hasNestedChild()) { Layer layer = (Layer) nested; nested = layer.child; diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java index a2fa2351..dee59058 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java @@ -37,6 +37,8 @@ import org.bouncycastle.openpgp.operator.PBEDataDecryptorFactory; import org.bouncycastle.openpgp.operator.PGPContentVerifierBuilderProvider; import org.bouncycastle.openpgp.operator.PublicKeyDataDecryptorFactory; import org.bouncycastle.openpgp.operator.SessionKeyDataDecryptorFactory; +import org.bouncycastle.util.io.Streams; +import org.bouncycastle.util.io.TeeInputStream; import org.pgpainless.PGPainless; import org.pgpainless.algorithm.CompressionAlgorithm; import org.pgpainless.algorithm.EncryptionPurpose; @@ -79,32 +81,87 @@ public class OpenPgpMessageInputStream extends DecryptionStream { // Options to consume the data protected final ConsumerOptions options; + + private final Policy policy; // Pushdown Automaton to verify validity of OpenPGP packet sequence in an OpenPGP message protected final PDA syntaxVerifier = new PDA(); // InputStream of OpenPGP packets protected TeeBCPGInputStream packetInputStream; - // InputStream of a nested data packet + // InputStream of a data packet containing nested data protected InputStream nestedInputStream; private boolean closed = false; private final Signatures signatures; private final MessageMetadata.Layer metadata; - private final Policy policy; - public OpenPgpMessageInputStream(@Nonnull InputStream inputStream, + /** + * Create an {@link OpenPgpMessageInputStream} suitable for decryption and verification of + * OpenPGP messages and signatures. + * This constructor will use the global PGPainless {@link Policy}. + * + * @param inputStream underlying input stream + * @param options options for consuming the stream + * + * @throws IOException in case of an IO error + * @throws PGPException in case of an OpenPGP error + */ + public static OpenPgpMessageInputStream create(@Nonnull InputStream inputStream, @Nonnull ConsumerOptions options) throws IOException, PGPException { - this(inputStream, options, PGPainless.getPolicy()); + return create(inputStream, options, PGPainless.getPolicy()); } - public OpenPgpMessageInputStream(@Nonnull InputStream inputStream, + /** + * Create an {@link OpenPgpMessageInputStream} suitable for decryption and verification of + * OpenPGP messages and signatures. + * + * @param inputStream underlying input stream containing the OpenPGP message + * @param options options for consuming the message + * @param policy policy for acceptable algorithms etc. + * + * @throws PGPException in case of an OpenPGP error + * @throws IOException in case of an IO error + */ + public static OpenPgpMessageInputStream create(@Nonnull InputStream inputStream, @Nonnull ConsumerOptions options, @Nonnull Policy policy) throws PGPException, IOException { - this( - prepareInputStream(inputStream, options, policy), - options, new MessageMetadata.Message(), policy); + return create(inputStream, options, new MessageMetadata.Message(), policy); + } + + protected static OpenPgpMessageInputStream create(@Nonnull InputStream inputStream, + @Nonnull ConsumerOptions options, + @Nonnull MessageMetadata.Layer metadata, + @Nonnull Policy policy) + throws IOException, PGPException { + OpenPgpInputStream openPgpIn = new OpenPgpInputStream(inputStream); + openPgpIn.reset(); + + if (openPgpIn.isNonOpenPgp() || options.isForceNonOpenPgpData()) { + return new OpenPgpMessageInputStream(Type.non_openpgp, + openPgpIn, options, metadata, policy); + } + + if (openPgpIn.isBinaryOpenPgp()) { + // Simply consume OpenPGP message + return new OpenPgpMessageInputStream(Type.standard, + openPgpIn, options, metadata, policy); + } + + if (openPgpIn.isAsciiArmored()) { + ArmoredInputStream armorIn = ArmoredInputStreamFactory.get(openPgpIn); + if (armorIn.isClearText()) { + return new OpenPgpMessageInputStream(Type.cleartext_signed, + armorIn, options, metadata, policy); + } else { + // Simply consume dearmored OpenPGP message + return new OpenPgpMessageInputStream(Type.standard, + armorIn, options, metadata, policy); + } + } else { + throw new AssertionError("Huh?"); + } } protected OpenPgpMessageInputStream(@Nonnull InputStream inputStream, @@ -131,52 +188,56 @@ public class OpenPgpMessageInputStream extends DecryptionStream { consumePackets(); } - protected OpenPgpMessageInputStream(@Nonnull InputStream inputStream, - @Nonnull Policy policy, - @Nonnull ConsumerOptions options) { + enum Type { + standard, + cleartext_signed, + non_openpgp + } + + protected OpenPgpMessageInputStream(@Nonnull Type type, + @Nonnull InputStream inputStream, + @Nonnull ConsumerOptions options, + @Nonnull MessageMetadata.Layer metadata, + @Nonnull Policy policy) throws PGPException, IOException { super(OpenPgpMetadata.getBuilder()); this.policy = policy; this.options = options; - this.metadata = new MessageMetadata.Message(); + this.metadata = metadata; this.signatures = new Signatures(options); - this.signatures.addDetachedSignatures(options.getDetachedSignatures()); - this.packetInputStream = new TeeBCPGInputStream(BCPGInputStream.wrap(inputStream), signatures); - } - private static InputStream prepareInputStream(InputStream inputStream, ConsumerOptions options, Policy policy) - throws IOException, PGPException { - OpenPgpInputStream openPgpIn = new OpenPgpInputStream(inputStream); - openPgpIn.reset(); - - if (openPgpIn.isBinaryOpenPgp()) { - return openPgpIn; + if (metadata instanceof MessageMetadata.Message) { + this.signatures.addDetachedSignatures(options.getDetachedSignatures()); } - if (openPgpIn.isAsciiArmored()) { - ArmoredInputStream armorIn = ArmoredInputStreamFactory.get(openPgpIn); - if (armorIn.isClearText()) { - return parseCleartextSignedMessage(armorIn, options, policy); - } else { - return armorIn; - } - } else { - return openPgpIn; + switch (type) { + case standard: + // tee out packet bytes for signature verification + packetInputStream = new TeeBCPGInputStream(BCPGInputStream.wrap(inputStream), this.signatures); + + // *omnomnom* + consumePackets(); + break; + case cleartext_signed: + MultiPassStrategy multiPassStrategy = options.getMultiPassStrategy(); + PGPSignatureList detachedSignatures = ClearsignedMessageUtil + .detachSignaturesFromInbandClearsignedMessage( + inputStream, multiPassStrategy.getMessageOutputStream()); + + for (PGPSignature signature : detachedSignatures) { + options.addVerificationOfDetachedSignature(signature); + } + + options.forceNonOpenPgpData(); + packetInputStream = null; + nestedInputStream = new TeeInputStream(multiPassStrategy.getMessageInputStream(), this.signatures); + break; + case non_openpgp: + packetInputStream = null; + nestedInputStream = new TeeInputStream(inputStream, this.signatures); + break; } } - private static DecryptionStream parseCleartextSignedMessage(ArmoredInputStream armorIn, ConsumerOptions options, Policy policy) - throws IOException, PGPException { - MultiPassStrategy multiPassStrategy = options.getMultiPassStrategy(); - PGPSignatureList signatures = ClearsignedMessageUtil.detachSignaturesFromInbandClearsignedMessage(armorIn, multiPassStrategy.getMessageOutputStream()); - - for (PGPSignature signature : signatures) { - options.addVerificationOfDetachedSignature(signature); - } - - options.forceNonOpenPgpData(); - return new OpenPgpMessageInputStream(multiPassStrategy.getMessageInputStream(), policy, options); - } - /** * Consume OpenPGP packets from the current {@link BCPGInputStream}. * Once an OpenPGP packet with nested data (Literal Data, Compressed Data, Encrypted Data) is reached, @@ -196,6 +257,9 @@ public class OpenPgpMessageInputStream extends DecryptionStream { private void consumePackets() throws IOException, PGPException { OpenPgpPacket nextPacket; + if (packetInputStream == null) { + return; + } loop: // we break this when we go deeper. while ((nextPacket = packetInputStream.nextPacketTag()) != null) { diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/TeeBCPGInputStream.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/TeeBCPGInputStream.java index 2efcfc43..1ca1b3f9 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/TeeBCPGInputStream.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/TeeBCPGInputStream.java @@ -4,6 +4,11 @@ package org.pgpainless.decryption_verification; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.NoSuchElementException; + import org.bouncycastle.bcpg.BCPGInputStream; import org.bouncycastle.bcpg.MarkerPacket; import org.bouncycastle.bcpg.Packet; @@ -15,11 +20,6 @@ import org.bouncycastle.openpgp.PGPOnePassSignature; import org.bouncycastle.openpgp.PGPSignature; import org.pgpainless.algorithm.OpenPgpPacket; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.util.NoSuchElementException; - /** * Since we need to update signatures with data from the underlying stream, this class is used to tee out the data. * Unfortunately we cannot simply override {@link BCPGInputStream#read()} to tee the data out though, since @@ -96,7 +96,6 @@ public class TeeBCPGInputStream { return markerPacket; } - public void close() throws IOException { this.packetInputStream.close(); this.delayedTee.close(); @@ -122,6 +121,9 @@ public class TeeBCPGInputStream { last = inputStream.read(); return last; } catch (IOException e) { + if ("crc check failed in armored message.".equals(e.getMessage())) { + throw e; + } return -1; } } diff --git a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStreamTest.java b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStreamTest.java index a8969047..f9539149 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStreamTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStreamTest.java @@ -677,7 +677,7 @@ public class OpenPgpMessageInputStreamTest { throws IOException, PGPException { ByteArrayInputStream bytesIn = new ByteArrayInputStream(armored.getBytes(StandardCharsets.UTF_8)); ArmoredInputStream armorIn = ArmoredInputStreamFactory.get(bytesIn); - OpenPgpMessageInputStream pgpIn = new OpenPgpMessageInputStream(armorIn, options); + OpenPgpMessageInputStream pgpIn = OpenPgpMessageInputStream.create(armorIn, options); return pgpIn; } } From e281143d4873f4f2d3f2623b57471a622c54dba3 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Fri, 21 Oct 2022 16:17:30 +0200 Subject: [PATCH 061/763] Delete old DecryptionStreamFactory --- .../DecryptionStreamFactory.java | 692 ------------------ 1 file changed, 692 deletions(-) delete mode 100644 pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionStreamFactory.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 deleted file mode 100644 index 451750ee..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionStreamFactory.java +++ /dev/null @@ -1,692 +0,0 @@ -// SPDX-FileCopyrightText: 2018 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.decryption_verification; - -import java.io.IOException; -import java.io.InputStream; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Iterator; -import java.util.List; -import java.util.Map; -import java.util.NoSuchElementException; -import java.util.Set; -import javax.annotation.Nonnull; -import javax.annotation.Nullable; - -import org.bouncycastle.bcpg.ArmoredInputStream; -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; -import org.bouncycastle.openpgp.PGPPublicKeyRing; -import org.bouncycastle.openpgp.PGPSecretKey; -import org.bouncycastle.openpgp.PGPSecretKeyRing; -import org.bouncycastle.openpgp.PGPSessionKey; -import org.bouncycastle.openpgp.PGPSignature; -import org.bouncycastle.openpgp.PGPSignatureList; -import org.bouncycastle.openpgp.operator.PBEDataDecryptorFactory; -import org.bouncycastle.openpgp.operator.PGPContentVerifierBuilderProvider; -import org.bouncycastle.openpgp.operator.PublicKeyDataDecryptorFactory; -import org.bouncycastle.openpgp.operator.SessionKeyDataDecryptorFactory; -import org.pgpainless.PGPainless; -import org.pgpainless.algorithm.CompressionAlgorithm; -import org.pgpainless.algorithm.EncryptionPurpose; -import org.pgpainless.algorithm.StreamEncoding; -import org.pgpainless.algorithm.SymmetricKeyAlgorithm; -import org.pgpainless.decryption_verification.cleartext_signatures.ClearsignedMessageUtil; -import org.pgpainless.decryption_verification.cleartext_signatures.MultiPassStrategy; -import org.pgpainless.exception.FinalIOException; -import org.pgpainless.exception.MessageNotIntegrityProtectedException; -import org.pgpainless.exception.MissingDecryptionMethodException; -import org.pgpainless.exception.MissingLiteralDataException; -import org.pgpainless.exception.MissingPassphraseException; -import org.pgpainless.exception.SignatureValidationException; -import org.pgpainless.exception.UnacceptableAlgorithmException; -import org.pgpainless.implementation.ImplementationFactory; -import org.pgpainless.key.SubkeyIdentifier; -import org.pgpainless.key.info.KeyRingInfo; -import org.pgpainless.key.protection.SecretKeyRingProtector; -import org.pgpainless.key.protection.UnlockSecretKey; -import org.pgpainless.signature.SignatureUtils; -import org.pgpainless.signature.consumer.SignatureCheck; -import org.pgpainless.signature.consumer.OnePassSignatureCheck; -import org.pgpainless.util.ArmoredInputStreamFactory; -import org.pgpainless.util.Passphrase; -import org.pgpainless.util.SessionKey; -import org.pgpainless.util.Tuple; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -public final class DecryptionStreamFactory { - - - private static final Logger LOGGER = LoggerFactory.getLogger(DecryptionStreamFactory.class); - // Maximum nesting depth of packets (e.g. compression, encryption...) - private static final int MAX_PACKET_NESTING_DEPTH = 16; - - private final ConsumerOptions options; - private final OpenPgpMetadata.Builder resultBuilder = OpenPgpMetadata.getBuilder(); - private final List onePassSignatureChecks = new ArrayList<>(); - private final List signatureChecks = new ArrayList<>(); - private final Map onePassSignaturesWithMissingCert = new HashMap<>(); - - private static final PGPContentVerifierBuilderProvider verifierBuilderProvider = - ImplementationFactory.getInstance().getPGPContentVerifierBuilderProvider(); - private IntegrityProtectedInputStream integrityProtectedEncryptedInputStream; - - - public static DecryptionStream create(@Nonnull InputStream inputStream, - @Nonnull ConsumerOptions options) - throws PGPException, IOException { - OpenPgpInputStream openPgpInputStream = new OpenPgpInputStream(inputStream); - openPgpInputStream.reset(); - if (openPgpInputStream.isBinaryOpenPgp()) { - return new OpenPgpMessageInputStream(openPgpInputStream, options); - } else if (openPgpInputStream.isAsciiArmored()) { - ArmoredInputStream armorIn = ArmoredInputStreamFactory.get(openPgpInputStream); - if (armorIn.isClearText()) { - return createOld(openPgpInputStream, options); - } else { - return new OpenPgpMessageInputStream(armorIn, options); - } - } else if (openPgpInputStream.isNonOpenPgp()) { - return createOld(openPgpInputStream, options); - } else { - throw new IOException("What?"); - } - } - - public static DecryptionStream createOld(@Nonnull InputStream inputStream, - @Nonnull ConsumerOptions options) throws IOException, PGPException { - DecryptionStreamFactory factory = new DecryptionStreamFactory(options); - OpenPgpInputStream openPgpIn = new OpenPgpInputStream(inputStream); - return factory.parseOpenPGPDataAndCreateDecryptionStream(openPgpIn); - } - - public DecryptionStreamFactory(ConsumerOptions options) { - this.options = options; - initializeDetachedSignatures(options.getDetachedSignatures()); - } - - private void initializeDetachedSignatures(Set signatures) { - for (PGPSignature signature : signatures) { - long issuerKeyId = SignatureUtils.determineIssuerKeyId(signature); - PGPPublicKeyRing signingKeyRing = findSignatureVerificationKeyRing(issuerKeyId); - if (signingKeyRing == null) { - SignatureValidationException ex = new SignatureValidationException( - "Missing verification certificate " + Long.toHexString(issuerKeyId)); - resultBuilder.addInvalidDetachedSignature(new SignatureVerification(signature, null), ex); - continue; - } - PGPPublicKey signingKey = signingKeyRing.getPublicKey(issuerKeyId); - SubkeyIdentifier signingKeyIdentifier = new SubkeyIdentifier(signingKeyRing, signingKey.getKeyID()); - try { - signature.init(verifierBuilderProvider, signingKey); - SignatureCheck detachedSignature = - new SignatureCheck(signature, signingKeyRing, signingKeyIdentifier); - signatureChecks.add(detachedSignature); - } catch (PGPException e) { - SignatureValidationException ex = new SignatureValidationException( - "Cannot verify detached signature made by " + signingKeyIdentifier + ".", e); - resultBuilder.addInvalidDetachedSignature(new SignatureVerification(signature, signingKeyIdentifier), ex); - } - } - } - - private DecryptionStream parseOpenPGPDataAndCreateDecryptionStream(OpenPgpInputStream openPgpIn) - throws IOException, PGPException { - - InputStream pgpInStream; - InputStream outerDecodingStream; - PGPObjectFactory objectFactory; - - // Non-OpenPGP data. We are probably verifying detached signatures - if (openPgpIn.isNonOpenPgp() || options.isForceNonOpenPgpData()) { - outerDecodingStream = openPgpIn; - pgpInStream = wrapInVerifySignatureStream(outerDecodingStream, null); - return new DecryptionStreamImpl(pgpInStream, resultBuilder, integrityProtectedEncryptedInputStream, null); - } - - // Data appears to be OpenPGP message, - // or we handle it as such, since user provided a session-key for decryption - if (openPgpIn.isLikelyOpenPgpMessage() || - (openPgpIn.isBinaryOpenPgp() && options.getSessionKey() != null)) { - outerDecodingStream = openPgpIn; - objectFactory = ImplementationFactory.getInstance().getPGPObjectFactory(outerDecodingStream); - // Parse OpenPGP message - pgpInStream = processPGPPackets(objectFactory, 1); - return new DecryptionStreamImpl(pgpInStream, - resultBuilder, integrityProtectedEncryptedInputStream, null); - } - - if (openPgpIn.isAsciiArmored()) { - ArmoredInputStream armoredInputStream = ArmoredInputStreamFactory.get(openPgpIn); - if (armoredInputStream.isClearText()) { - resultBuilder.setCleartextSigned(); - return parseCleartextSignedMessage(armoredInputStream); - } else { - outerDecodingStream = armoredInputStream; - objectFactory = ImplementationFactory.getInstance().getPGPObjectFactory(outerDecodingStream); - // Parse OpenPGP message - pgpInStream = processPGPPackets(objectFactory, 1); - return new DecryptionStreamImpl(pgpInStream, - resultBuilder, integrityProtectedEncryptedInputStream, - outerDecodingStream); - } - } - - throw new PGPException("Not sure how to handle the input stream."); - } - - private DecryptionStreamImpl parseCleartextSignedMessage(ArmoredInputStream armorIn) - throws IOException, PGPException { - resultBuilder.setCompressionAlgorithm(CompressionAlgorithm.UNCOMPRESSED) - .setFileEncoding(StreamEncoding.TEXT); - - MultiPassStrategy multiPassStrategy = options.getMultiPassStrategy(); - PGPSignatureList signatures = ClearsignedMessageUtil.detachSignaturesFromInbandClearsignedMessage(armorIn, multiPassStrategy.getMessageOutputStream()); - - for (PGPSignature signature : signatures) { - options.addVerificationOfDetachedSignature(signature); - } - - initializeDetachedSignatures(options.getDetachedSignatures()); - - InputStream verifyIn = wrapInVerifySignatureStream(multiPassStrategy.getMessageInputStream(), null); - return new DecryptionStreamImpl(verifyIn, resultBuilder, integrityProtectedEncryptedInputStream, - null); - } - - private InputStream wrapInVerifySignatureStream(InputStream bufferedIn, @Nullable PGPObjectFactory objectFactory) { - return new SignatureInputStream.VerifySignatures( - bufferedIn, objectFactory, onePassSignatureChecks, - onePassSignaturesWithMissingCert, signatureChecks, options, - resultBuilder); - } - - private InputStream processPGPPackets(@Nonnull PGPObjectFactory objectFactory, int depth) - throws IOException, PGPException { - if (depth >= MAX_PACKET_NESTING_DEPTH) { - throw new PGPException("Maximum depth of nested packages exceeded."); - } - Object nextPgpObject; - try { - while ((nextPgpObject = objectFactory.nextObject()) != null) { - if (nextPgpObject instanceof PGPEncryptedDataList) { - return processPGPEncryptedDataList((PGPEncryptedDataList) nextPgpObject, depth); - } - if (nextPgpObject instanceof PGPCompressedData) { - return processPGPCompressedData((PGPCompressedData) nextPgpObject, depth); - } - if (nextPgpObject instanceof PGPOnePassSignatureList) { - return processOnePassSignatureList(objectFactory, (PGPOnePassSignatureList) nextPgpObject, depth); - } - if (nextPgpObject instanceof PGPLiteralData) { - return processPGPLiteralData(objectFactory, (PGPLiteralData) nextPgpObject, depth); - } - } - } catch (FinalIOException e) { - throw e; - } catch (IOException e) { - if (depth == 1 && e.getMessage().contains("invalid armor")) { - throw e; - } - if (depth == 1 && e.getMessage().contains("unknown object in stream:")) { - throw new MissingLiteralDataException("No Literal Data Packet found."); - } else { - throw new FinalIOException(e); - } - } - - throw new MissingLiteralDataException("No Literal Data Packet found"); - } - - private InputStream processPGPEncryptedDataList(PGPEncryptedDataList pgpEncryptedDataList, int depth) - throws PGPException, IOException { - LOGGER.debug("Depth {}: Encountered PGPEncryptedDataList", depth); - - SessionKey sessionKey = options.getSessionKey(); - if (sessionKey != null) { - integrityProtectedEncryptedInputStream = decryptWithProvidedSessionKey(pgpEncryptedDataList, sessionKey); - PGPObjectFactory factory = ImplementationFactory.getInstance().getPGPObjectFactory(integrityProtectedEncryptedInputStream); - return processPGPPackets(factory, ++depth); - } - - InputStream decryptedDataStream = decryptSessionKey(pgpEncryptedDataList); - PGPObjectFactory factory = ImplementationFactory.getInstance().getPGPObjectFactory(decryptedDataStream); - return processPGPPackets(factory, ++depth); - } - - private IntegrityProtectedInputStream decryptWithProvidedSessionKey( - PGPEncryptedDataList pgpEncryptedDataList, - SessionKey sessionKey) - throws PGPException { - SessionKeyDataDecryptorFactory decryptorFactory = ImplementationFactory.getInstance() - .getSessionKeyDataDecryptorFactory(sessionKey); - InputStream decryptedDataStream = null; - PGPEncryptedData encryptedData = null; - for (PGPEncryptedData pgpEncryptedData : pgpEncryptedDataList) { - encryptedData = pgpEncryptedData; - if (!options.isIgnoreMDCErrors() && !encryptedData.isIntegrityProtected()) { - throw new MessageNotIntegrityProtectedException(); - } - - if (encryptedData instanceof PGPPBEEncryptedData) { - PGPPBEEncryptedData pbeEncrypted = (PGPPBEEncryptedData) encryptedData; - decryptedDataStream = pbeEncrypted.getDataStream(decryptorFactory); - break; - } else if (encryptedData instanceof PGPPublicKeyEncryptedData) { - PGPPublicKeyEncryptedData pkEncrypted = (PGPPublicKeyEncryptedData) encryptedData; - decryptedDataStream = pkEncrypted.getDataStream(decryptorFactory); - break; - } - } - - if (decryptedDataStream == null) { - throw new PGPException("No valid PGP data encountered."); - } - - resultBuilder.setSessionKey(sessionKey); - throwIfAlgorithmIsRejected(sessionKey.getAlgorithm()); - integrityProtectedEncryptedInputStream = - new IntegrityProtectedInputStream(decryptedDataStream, encryptedData, options); - return integrityProtectedEncryptedInputStream; - } - - private InputStream processPGPCompressedData(PGPCompressedData pgpCompressedData, int depth) - throws PGPException, IOException { - try { - CompressionAlgorithm compressionAlgorithm = CompressionAlgorithm.requireFromId(pgpCompressedData.getAlgorithm()); - LOGGER.debug("Depth {}: Encountered PGPCompressedData: {}", depth, compressionAlgorithm); - resultBuilder.setCompressionAlgorithm(compressionAlgorithm); - } catch (NoSuchElementException e) { - throw new PGPException("Unknown compression algorithm encountered.", e); - } - - InputStream inflatedDataStream = pgpCompressedData.getDataStream(); - PGPObjectFactory objectFactory = ImplementationFactory.getInstance().getPGPObjectFactory(inflatedDataStream); - - return processPGPPackets(objectFactory, ++depth); - } - - private InputStream processOnePassSignatureList( - @Nonnull PGPObjectFactory objectFactory, - PGPOnePassSignatureList onePassSignatures, - int depth) - throws PGPException, IOException { - LOGGER.debug("Depth {}: Encountered PGPOnePassSignatureList of size {}", depth, onePassSignatures.size()); - initOnePassSignatures(onePassSignatures); - return processPGPPackets(objectFactory, depth); - } - - private InputStream processPGPLiteralData( - @Nonnull PGPObjectFactory objectFactory, - PGPLiteralData pgpLiteralData, - int depth) { - LOGGER.debug("Depth {}: Found PGPLiteralData", depth); - InputStream literalDataInputStream = pgpLiteralData.getInputStream(); - - resultBuilder.setFileName(pgpLiteralData.getFileName()) - .setModificationDate(pgpLiteralData.getModificationTime()) - .setFileEncoding(StreamEncoding.requireFromCode(pgpLiteralData.getFormat())); - - if (onePassSignatureChecks.isEmpty() && onePassSignaturesWithMissingCert.isEmpty()) { - LOGGER.debug("No OnePassSignatures found -> We are done"); - return literalDataInputStream; - } - - return new SignatureInputStream.VerifySignatures(literalDataInputStream, objectFactory, - onePassSignatureChecks, onePassSignaturesWithMissingCert, signatureChecks, options, resultBuilder) { - }; - } - - private InputStream decryptSessionKey(@Nonnull PGPEncryptedDataList encryptedDataList) - throws PGPException { - Iterator encryptedDataIterator = encryptedDataList.getEncryptedDataObjects(); - if (!encryptedDataIterator.hasNext()) { - throw new PGPException("Decryption failed - EncryptedDataList has no items"); - } - - PGPPrivateKey decryptionKey = null; - PGPPublicKeyEncryptedData encryptedSessionKey = null; - - List passphraseProtected = new ArrayList<>(); - List publicKeyProtected = new ArrayList<>(); - List> postponedDueToMissingPassphrase = new ArrayList<>(); - - // Sort PKESK and SKESK packets - while (encryptedDataIterator.hasNext()) { - PGPEncryptedData encryptedData = encryptedDataIterator.next(); - - if (!encryptedData.isIntegrityProtected() && !options.isIgnoreMDCErrors()) { - throw new MessageNotIntegrityProtectedException(); - } - - // SKESK - if (encryptedData instanceof PGPPBEEncryptedData) { - passphraseProtected.add((PGPPBEEncryptedData) encryptedData); - } - // PKESK - else if (encryptedData instanceof PGPPublicKeyEncryptedData) { - publicKeyProtected.add((PGPPublicKeyEncryptedData) encryptedData); - } - } - - // Try decryption with passphrases first - for (PGPPBEEncryptedData pbeEncryptedData : passphraseProtected) { - for (Passphrase passphrase : options.getDecryptionPassphrases()) { - PBEDataDecryptorFactory passphraseDecryptor = ImplementationFactory.getInstance() - .getPBEDataDecryptorFactory(passphrase); - try { - InputStream decryptedDataStream = pbeEncryptedData.getDataStream(passphraseDecryptor); - - PGPSessionKey pgpSessionKey = pbeEncryptedData.getSessionKey(passphraseDecryptor); - SessionKey sessionKey = new SessionKey(pgpSessionKey); - resultBuilder.setSessionKey(sessionKey); - - throwIfAlgorithmIsRejected(sessionKey.getAlgorithm()); - - integrityProtectedEncryptedInputStream = - new IntegrityProtectedInputStream(decryptedDataStream, pbeEncryptedData, options); - - return integrityProtectedEncryptedInputStream; - } catch (PGPException e) { - LOGGER.debug("Probable passphrase mismatch, skip PBE encrypted data block", e); - } - } - } - - // Try custom PublicKeyDataDecryptorFactories (e.g. hardware-backed). - Map customFactories = options.getCustomDecryptorFactories(); - for (PGPPublicKeyEncryptedData publicKeyEncryptedData : publicKeyProtected) { - Long keyId = publicKeyEncryptedData.getKeyID(); - if (!customFactories.containsKey(keyId)) { - continue; - } - - PublicKeyDataDecryptorFactory decryptorFactory = customFactories.get(keyId); - try { - InputStream decryptedDataStream = publicKeyEncryptedData.getDataStream(decryptorFactory); - PGPSessionKey pgpSessionKey = publicKeyEncryptedData.getSessionKey(decryptorFactory); - SessionKey sessionKey = new SessionKey(pgpSessionKey); - resultBuilder.setSessionKey(sessionKey); - - throwIfAlgorithmIsRejected(sessionKey.getAlgorithm()); - - integrityProtectedEncryptedInputStream = - new IntegrityProtectedInputStream(decryptedDataStream, publicKeyEncryptedData, options); - - return integrityProtectedEncryptedInputStream; - } catch (PGPException e) { - LOGGER.debug("Decryption with custom PublicKeyDataDecryptorFactory failed", e); - } - } - - // Then try decryption with public key encryption - for (PGPPublicKeyEncryptedData publicKeyEncryptedData : publicKeyProtected) { - PGPPrivateKey privateKey = null; - if (options.getDecryptionKeys().isEmpty()) { - break; - } - - long keyId = publicKeyEncryptedData.getKeyID(); - // Wildcard KeyID - if (keyId == 0L) { - LOGGER.debug("Hidden recipient detected. Try to decrypt with all available secret keys."); - for (PGPSecretKeyRing secretKeys : options.getDecryptionKeys()) { - if (privateKey != null) { - break; - } - KeyRingInfo info = new KeyRingInfo(secretKeys); - List encryptionSubkeys = info.getEncryptionSubkeys(EncryptionPurpose.ANY); - for (PGPPublicKey pubkey : encryptionSubkeys) { - PGPSecretKey secretKey = secretKeys.getSecretKey(pubkey.getKeyID()); - // Skip missing secret key - if (secretKey == null) { - continue; - } - - privateKey = tryPublicKeyDecryption(secretKeys, secretKey, publicKeyEncryptedData, - postponedDueToMissingPassphrase, true); - } - } - } - // Non-wildcard key-id - else { - LOGGER.debug("PGPEncryptedData is encrypted for key {}", Long.toHexString(keyId)); - resultBuilder.addRecipientKeyId(keyId); - - PGPSecretKeyRing secretKeys = findDecryptionKeyRing(keyId); - if (secretKeys == null) { - LOGGER.debug("Missing certificate of {}. Skip.", Long.toHexString(keyId)); - continue; - } - - // Make sure that the recipient key is encryption capable and non-expired - KeyRingInfo info = new KeyRingInfo(secretKeys); - List encryptionSubkeys = info.getEncryptionSubkeys(EncryptionPurpose.ANY); - - PGPSecretKey secretKey = null; - for (PGPPublicKey pubkey : encryptionSubkeys) { - if (pubkey.getKeyID() == keyId) { - secretKey = secretKeys.getSecretKey(keyId); - break; - } - } - - if (secretKey == null) { - LOGGER.debug("Key " + Long.toHexString(keyId) + " is not valid or not capable for decryption."); - } else { - privateKey = tryPublicKeyDecryption(secretKeys, secretKey, publicKeyEncryptedData, - postponedDueToMissingPassphrase, true); - } - } - if (privateKey == null) { - continue; - } - decryptionKey = privateKey; - encryptedSessionKey = publicKeyEncryptedData; - break; - } - - // Try postponed keys with missing passphrases (will cause missing passphrase callbacks to fire) - if (encryptedSessionKey == null) { - - if (options.getMissingKeyPassphraseStrategy() == MissingKeyPassphraseStrategy.THROW_EXCEPTION) { - // Non-interactive mode: Throw an exception with all locked decryption keys - Set keyIds = new HashSet<>(); - for (Tuple k : postponedDueToMissingPassphrase) { - keyIds.add(k.getA()); - } - if (!keyIds.isEmpty()) { - throw new MissingPassphraseException(keyIds); - } - } - else if (options.getMissingKeyPassphraseStrategy() == MissingKeyPassphraseStrategy.INTERACTIVE) { - // Interactive mode: Fire protector callbacks to get passphrases interactively - for (Tuple missingPassphrases : postponedDueToMissingPassphrase) { - SubkeyIdentifier keyId = missingPassphrases.getA(); - PGPPublicKeyEncryptedData publicKeyEncryptedData = missingPassphrases.getB(); - PGPSecretKeyRing secretKeys = findDecryptionKeyRing(keyId.getKeyId()); - PGPSecretKey secretKey = secretKeys.getSecretKey(keyId.getSubkeyId()); - - PGPPrivateKey privateKey = tryPublicKeyDecryption(secretKeys, secretKey, publicKeyEncryptedData, - postponedDueToMissingPassphrase, false); - if (privateKey == null) { - continue; - } - - decryptionKey = privateKey; - encryptedSessionKey = publicKeyEncryptedData; - break; - } - } else { - throw new IllegalStateException("Invalid PostponedKeysStrategy set in consumer options."); - } - - } - - return decryptWith(encryptedSessionKey, decryptionKey); - } - - /** - * Try decryption of the provided public-key-encrypted-data using the given secret key. - * If the secret key is encrypted and the secret key protector does not have a passphrase available and the boolean - * postponeIfMissingPassphrase is true, data decryption is postponed by pushing a tuple of the encrypted data decryption key - * identifier to the postponed list. - * - * This method only returns a non-null private key, if the private key is able to decrypt the message successfully. - * - * @param secretKeys secret key ring - * @param secretKey secret key - * @param publicKeyEncryptedData encrypted data which is tried to decrypt using the secret key - * @param postponed list of postponed decryptions due to missing secret key passphrases - * @param postponeIfMissingPassphrase flag to specify whether missing secret key passphrases should result in postponed decryption - * @return private key if decryption is successful, null if decryption is unsuccessful or postponed - * - * @throws PGPException in case of an OpenPGP error - */ - private PGPPrivateKey tryPublicKeyDecryption( - PGPSecretKeyRing secretKeys, - PGPSecretKey secretKey, - PGPPublicKeyEncryptedData publicKeyEncryptedData, - List> postponed, - boolean postponeIfMissingPassphrase) throws PGPException { - SecretKeyRingProtector protector = options.getSecretKeyProtector(secretKeys); - - if (postponeIfMissingPassphrase && !protector.hasPassphraseFor(secretKey.getKeyID())) { - // Postpone decryption with key with missing passphrase - SubkeyIdentifier identifier = new SubkeyIdentifier(secretKeys, secretKey.getKeyID()); - postponed.add(new Tuple<>(identifier, publicKeyEncryptedData)); - return null; - } - - PGPPrivateKey privateKey = UnlockSecretKey.unlockSecretKey( - secretKey, protector.getDecryptor(secretKey.getKeyID())); - - // test if we have the right private key - PublicKeyDataDecryptorFactory decryptorFactory = ImplementationFactory.getInstance() - .getPublicKeyDataDecryptorFactory(privateKey); - try { - publicKeyEncryptedData.getSymmetricAlgorithm(decryptorFactory); // will only succeed if we have the right secret key - LOGGER.debug("Found correct decryption key {}.", Long.toHexString(secretKey.getKeyID())); - resultBuilder.setDecryptionKey(new SubkeyIdentifier(secretKeys, privateKey.getKeyID())); - return privateKey; - } catch (PGPException | ClassCastException e) { - return null; - } - } - - private InputStream decryptWith(PGPPublicKeyEncryptedData encryptedSessionKey, PGPPrivateKey decryptionKey) - throws PGPException { - if (decryptionKey == null || encryptedSessionKey == null) { - throw new MissingDecryptionMethodException("Decryption failed - No suitable decryption key or passphrase found"); - } - - PublicKeyDataDecryptorFactory dataDecryptor = ImplementationFactory.getInstance() - .getPublicKeyDataDecryptorFactory(decryptionKey); - - PGPSessionKey pgpSessionKey = encryptedSessionKey.getSessionKey(dataDecryptor); - SessionKey sessionKey = new SessionKey(pgpSessionKey); - resultBuilder.setSessionKey(sessionKey); - - SymmetricKeyAlgorithm symmetricKeyAlgorithm = sessionKey.getAlgorithm(); - if (symmetricKeyAlgorithm == SymmetricKeyAlgorithm.NULL) { - LOGGER.debug("Message is unencrypted"); - } else { - LOGGER.debug("Message is encrypted using {}", symmetricKeyAlgorithm); - } - throwIfAlgorithmIsRejected(symmetricKeyAlgorithm); - - integrityProtectedEncryptedInputStream = new IntegrityProtectedInputStream( - encryptedSessionKey.getDataStream(dataDecryptor), encryptedSessionKey, options); - return integrityProtectedEncryptedInputStream; - } - - private void throwIfAlgorithmIsRejected(SymmetricKeyAlgorithm algorithm) - throws UnacceptableAlgorithmException { - if (!PGPainless.getPolicy().getSymmetricKeyDecryptionAlgorithmPolicy().isAcceptable(algorithm)) { - throw new UnacceptableAlgorithmException("Data is " - + (algorithm == SymmetricKeyAlgorithm.NULL ? - "unencrypted" : - "encrypted with symmetric algorithm " + algorithm) + " which is not acceptable as per PGPainless' policy.\n" + - "To mark this algorithm as acceptable, use PGPainless.getPolicy().setSymmetricKeyDecryptionAlgorithmPolicy()."); - } - } - - private void initOnePassSignatures(@Nonnull PGPOnePassSignatureList onePassSignatureList) - throws PGPException { - Iterator iterator = onePassSignatureList.iterator(); - if (!iterator.hasNext()) { - throw new PGPException("Verification failed - No OnePassSignatures found"); - } - - processOnePassSignatures(iterator); - } - - private void processOnePassSignatures(Iterator signatures) - throws PGPException { - while (signatures.hasNext()) { - PGPOnePassSignature signature = signatures.next(); - processOnePassSignature(signature); - } - } - - private void processOnePassSignature(PGPOnePassSignature signature) - throws PGPException { - final long keyId = signature.getKeyID(); - - LOGGER.debug("Encountered OnePassSignature from {}", Long.toHexString(keyId)); - - // Find public key - PGPPublicKeyRing verificationKeyRing = findSignatureVerificationKeyRing(keyId); - if (verificationKeyRing == null) { - onePassSignaturesWithMissingCert.put(keyId, new OnePassSignatureCheck(signature, null)); - return; - } - PGPPublicKey verificationKey = verificationKeyRing.getPublicKey(keyId); - - signature.init(verifierBuilderProvider, verificationKey); - OnePassSignatureCheck onePassSignature = new OnePassSignatureCheck(signature, verificationKeyRing); - onePassSignatureChecks.add(onePassSignature); - } - - private PGPSecretKeyRing findDecryptionKeyRing(long keyId) { - for (PGPSecretKeyRing key : options.getDecryptionKeys()) { - if (key.getSecretKey(keyId) != null) { - return key; - } - } - return null; - } - - private PGPPublicKeyRing findSignatureVerificationKeyRing(long keyId) { - PGPPublicKeyRing verificationKeyRing = null; - for (PGPPublicKeyRing publicKeyRing : options.getCertificates()) { - PGPPublicKey verificationKey = publicKeyRing.getPublicKey(keyId); - if (verificationKey != null) { - LOGGER.debug("Found public key {} for signature verification", Long.toHexString(keyId)); - verificationKeyRing = publicKeyRing; - break; - } - } - - if (verificationKeyRing == null && options.getMissingCertificateCallback() != null) { - verificationKeyRing = options.getMissingCertificateCallback().onMissingPublicKeyEncountered(keyId); - } - - return verificationKeyRing; - } -} From aa398f9963a7704c0da74c9227b561f495f85fcf Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 24 Oct 2022 17:49:30 +0200 Subject: [PATCH 062/763] Only check message integrity once --- .../IntegrityProtectedInputStream.java | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/IntegrityProtectedInputStream.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/IntegrityProtectedInputStream.java index 4da52d0f..286160e8 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/IntegrityProtectedInputStream.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/IntegrityProtectedInputStream.java @@ -11,12 +11,17 @@ import javax.annotation.Nonnull; import org.bouncycastle.openpgp.PGPEncryptedData; import org.bouncycastle.openpgp.PGPException; import org.pgpainless.exception.ModificationDetectionException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; public class IntegrityProtectedInputStream extends InputStream { + private static final Logger LOGGER = LoggerFactory.getLogger(IntegrityProtectedInputStream.class); + private final InputStream inputStream; private final PGPEncryptedData encryptedData; private final ConsumerOptions options; + private boolean closed = false; public IntegrityProtectedInputStream(InputStream inputStream, PGPEncryptedData encryptedData, ConsumerOptions options) { this.inputStream = inputStream; @@ -36,11 +41,17 @@ public class IntegrityProtectedInputStream extends InputStream { @Override public void close() throws IOException { + if (closed) { + return; + } + closed = true; + if (encryptedData.isIntegrityProtected() && !options.isIgnoreMDCErrors()) { try { if (!encryptedData.verify()) { throw new ModificationDetectionException(); } + LOGGER.debug("Integrity Protection check passed"); } catch (PGPException e) { throw new IOException("Failed to verify integrity protection", e); } From e0b21457931aa024a93ff02aa020e7f3022a4e44 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 24 Oct 2022 17:56:33 +0200 Subject: [PATCH 063/763] Fix more tests --- .../OpenPgpMessageInputStream.java | 76 ++++++++++++++----- .../TeeBCPGInputStream.java | 1 - 2 files changed, 59 insertions(+), 18 deletions(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java index dee59058..de80f7ff 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java @@ -4,7 +4,6 @@ package org.pgpainless.decryption_verification; -import java.io.BufferedInputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; @@ -37,7 +36,6 @@ import org.bouncycastle.openpgp.operator.PBEDataDecryptorFactory; import org.bouncycastle.openpgp.operator.PGPContentVerifierBuilderProvider; import org.bouncycastle.openpgp.operator.PublicKeyDataDecryptorFactory; import org.bouncycastle.openpgp.operator.SessionKeyDataDecryptorFactory; -import org.bouncycastle.util.io.Streams; import org.bouncycastle.util.io.TeeInputStream; import org.pgpainless.PGPainless; import org.pgpainless.algorithm.CompressionAlgorithm; @@ -210,6 +208,7 @@ public class OpenPgpMessageInputStream extends DecryptionStream { } switch (type) { + case standard: // tee out packet bytes for signature verification packetInputStream = new TeeBCPGInputStream(BCPGInputStream.wrap(inputStream), this.signatures); @@ -217,20 +216,22 @@ public class OpenPgpMessageInputStream extends DecryptionStream { // *omnomnom* consumePackets(); break; + case cleartext_signed: + resultBuilder.setCleartextSigned(); MultiPassStrategy multiPassStrategy = options.getMultiPassStrategy(); PGPSignatureList detachedSignatures = ClearsignedMessageUtil .detachSignaturesFromInbandClearsignedMessage( inputStream, multiPassStrategy.getMessageOutputStream()); for (PGPSignature signature : detachedSignatures) { - options.addVerificationOfDetachedSignature(signature); + signatures.addDetachedSignature(signature); } options.forceNonOpenPgpData(); - packetInputStream = null; nestedInputStream = new TeeInputStream(multiPassStrategy.getMessageInputStream(), this.signatures); break; + case non_openpgp: packetInputStream = null; nestedInputStream = new TeeInputStream(inputStream, this.signatures); @@ -348,14 +349,14 @@ public class OpenPgpMessageInputStream extends DecryptionStream { metadata.depth + 1); LOGGER.debug("Compressed Data Packet (" + compressionLayer.algorithm + ") at depth " + metadata.depth + " encountered"); InputStream decompressed = compressedData.getDataStream(); - nestedInputStream = new OpenPgpMessageInputStream(buffer(decompressed), options, compressionLayer, policy); + nestedInputStream = new OpenPgpMessageInputStream(decompressed, options, compressionLayer, policy); } private void processOnePassSignature() throws PGPException, IOException { syntaxVerifier.next(InputAlphabet.OnePassSignature); PGPOnePassSignature onePassSignature = packetInputStream.readOnePassSignature(); LOGGER.debug("One-Pass-Signature Packet by key " + KeyIdUtil.formatKeyId(onePassSignature.getKeyID()) + - "at depth " + metadata.depth + " encountered"); + " at depth " + metadata.depth + " encountered"); signatures.addOnePassSignature(onePassSignature); } @@ -434,7 +435,7 @@ public class OpenPgpMessageInputStream extends DecryptionStream { InputStream decrypted = skesk.getDataStream(decryptorFactory); encryptedData.sessionKey = sessionKey; IntegrityProtectedInputStream integrityProtected = new IntegrityProtectedInputStream(decrypted, skesk, options); - nestedInputStream = new OpenPgpMessageInputStream(buffer(integrityProtected), options, encryptedData, policy); + nestedInputStream = new OpenPgpMessageInputStream(integrityProtected, options, encryptedData, policy); LOGGER.debug("Successfully decrypted data with provided session key"); return true; } else if (esk instanceof PGPPublicKeyEncryptedData) { @@ -442,7 +443,7 @@ public class OpenPgpMessageInputStream extends DecryptionStream { InputStream decrypted = pkesk.getDataStream(decryptorFactory); encryptedData.sessionKey = sessionKey; IntegrityProtectedInputStream integrityProtected = new IntegrityProtectedInputStream(decrypted, pkesk, options); - nestedInputStream = new OpenPgpMessageInputStream(buffer(integrityProtected), options, encryptedData, policy); + nestedInputStream = new OpenPgpMessageInputStream(integrityProtected, options, encryptedData, policy); LOGGER.debug("Successfully decrypted data with provided session key"); return true; } else { @@ -579,7 +580,7 @@ public class OpenPgpMessageInputStream extends DecryptionStream { encryptedData.sessionKey = sessionKey; LOGGER.debug("Successfully decrypted data with passphrase"); IntegrityProtectedInputStream integrityProtected = new IntegrityProtectedInputStream(decrypted, skesk, options); - nestedInputStream = new OpenPgpMessageInputStream(buffer(integrityProtected), options, encryptedData, policy); + nestedInputStream = new OpenPgpMessageInputStream(integrityProtected, options, encryptedData, policy); return true; } catch (UnacceptableAlgorithmException e) { throw e; @@ -606,7 +607,7 @@ public class OpenPgpMessageInputStream extends DecryptionStream { LOGGER.debug("Successfully decrypted data with key " + decryptionKeyId); IntegrityProtectedInputStream integrityProtected = new IntegrityProtectedInputStream(decrypted, pkesk, options); - nestedInputStream = new OpenPgpMessageInputStream(buffer(integrityProtected), options, encryptedData, policy); + nestedInputStream = new OpenPgpMessageInputStream(integrityProtected, options, encryptedData, policy); return true; } catch (UnacceptableAlgorithmException e) { throw e; @@ -631,10 +632,6 @@ public class OpenPgpMessageInputStream extends DecryptionStream { } } - private static InputStream buffer(InputStream inputStream) { - return new BufferedInputStream(inputStream); - } - private List> findPotentialDecryptionKeys(PGPPublicKeyEncryptedData pkesk) { int algorithm = pkesk.getAlgorithm(); List> decryptionKeyCandidates = new ArrayList<>(); @@ -665,7 +662,9 @@ public class OpenPgpMessageInputStream extends DecryptionStream { @Override public int read() throws IOException { if (nestedInputStream == null) { - syntaxVerifier.assertValid(); + if (packetInputStream != null) { + syntaxVerifier.assertValid(); + } return -1; } @@ -699,7 +698,6 @@ public class OpenPgpMessageInputStream extends DecryptionStream { @Override public int read(@Nonnull byte[] b, int off, int len) throws IOException { - if (nestedInputStream == null) { if (packetInputStream != null) { syntaxVerifier.assertValid(); @@ -768,6 +766,7 @@ public class OpenPgpMessageInputStream extends DecryptionStream { if (!closed) { throw new IllegalStateException("Stream must be closed before access to metadata can be granted."); } + return new MessageMetadata((MessageMetadata.Message) metadata); } @@ -857,6 +856,9 @@ public class OpenPgpMessageInputStream extends DecryptionStream { final Stack> opsUpdateStack; List literalOPS = new ArrayList<>(); final List correspondingSignatures; + final List prependedSignaturesWithMissingCert = new ArrayList<>(); + final List inbandSignaturesWithMissingCert = new ArrayList<>(); + final List detachedSignaturesWithMissingCert = new ArrayList<>(); boolean isLiteral = true; private Signatures(ConsumerOptions options) { @@ -876,15 +878,29 @@ public class OpenPgpMessageInputStream extends DecryptionStream { void addDetachedSignature(PGPSignature signature) { SignatureCheck check = initializeSignature(signature); + long keyId = SignatureUtils.determineIssuerKeyId(signature); if (check != null) { detachedSignatures.add(check); + } else { + LOGGER.debug("No suitable certificate for verification of signature by key " + KeyIdUtil.formatKeyId(keyId) + " found."); + this.detachedSignaturesWithMissingCert.add(new SignatureVerification.Failure( + new SignatureVerification(signature, null), + new SignatureValidationException("Missing verification key") + )); } } void addPrependedSignature(PGPSignature signature) { SignatureCheck check = initializeSignature(signature); + long keyId = SignatureUtils.determineIssuerKeyId(signature); if (check != null) { this.prependedSignatures.add(check); + } else { + LOGGER.debug("No suitable certificate for verification of signature by key " + KeyIdUtil.formatKeyId(keyId) + " found."); + this.prependedSignaturesWithMissingCert.add(new SignatureVerification.Failure( + new SignatureVerification(signature, null), + new SignatureValidationException("Missing verification key") + )); } } @@ -916,11 +932,14 @@ public class OpenPgpMessageInputStream extends DecryptionStream { } void addCorrespondingOnePassSignature(PGPSignature signature, MessageMetadata.Layer layer, Policy policy) { + boolean found = false; + long keyId = SignatureUtils.determineIssuerKeyId(signature); for (int i = onePassSignatures.size() - 1; i >= 0; i--) { OnePassSignatureCheck onePassSignature = onePassSignatures.get(i); - if (onePassSignature.getOnePassSignature().getKeyID() != signature.getKeyID()) { + if (onePassSignature.getOnePassSignature().getKeyID() != keyId) { continue; } + found = true; if (onePassSignature.getSignature() != null) { continue; @@ -942,6 +961,13 @@ public class OpenPgpMessageInputStream extends DecryptionStream { } break; } + + if (!found) { + LOGGER.debug("No suitable certificate for verification of signature by key " + KeyIdUtil.formatKeyId(keyId) + " found."); + inbandSignaturesWithMissingCert.add(new SignatureVerification.Failure( + new SignatureVerification(signature, null), + new SignatureValidationException("Missing verification key."))); + } } void enterNesting() { @@ -983,6 +1009,10 @@ public class OpenPgpMessageInputStream extends DecryptionStream { return cert; } } + + if (options.getMissingCertificateCallback() != null) { + return options.getMissingCertificateCallback().onMissingPublicKeyEncountered(keyId); + } return null; // TODO: Missing cert for sig } @@ -1066,6 +1096,18 @@ public class OpenPgpMessageInputStream extends DecryptionStream { layer.addRejectedPrependedSignature(new SignatureVerification.Failure(verification, e)); } } + + for (SignatureVerification.Failure rejected : inbandSignaturesWithMissingCert) { + layer.addRejectedOnePassSignature(rejected); + } + + for (SignatureVerification.Failure rejected : prependedSignaturesWithMissingCert) { + layer.addRejectedPrependedSignature(rejected); + } + + for (SignatureVerification.Failure rejected : detachedSignaturesWithMissingCert) { + layer.addRejectedDetachedSignature(rejected); + } } @Override diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/TeeBCPGInputStream.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/TeeBCPGInputStream.java index 1ca1b3f9..bbcf593e 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/TeeBCPGInputStream.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/TeeBCPGInputStream.java @@ -98,7 +98,6 @@ public class TeeBCPGInputStream { public void close() throws IOException { this.packetInputStream.close(); - this.delayedTee.close(); } public static class DelayedTeeInputStreamInputStream extends InputStream { From 8097c87b7f8d3f9e310965cf79566db4a5d60663 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 24 Oct 2022 18:30:40 +0200 Subject: [PATCH 064/763] Fix last two broken tests --- .../OpenPgpMessageInputStream.java | 31 ++++++++++++------- .../org/pgpainless/key/util/KeyRingUtils.java | 11 +++++++ 2 files changed, 30 insertions(+), 12 deletions(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java index de80f7ff..11a61e8c 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java @@ -9,6 +9,7 @@ import java.io.InputStream; import java.io.OutputStream; import java.util.ArrayList; import java.util.Collection; +import java.util.Date; import java.util.HashSet; import java.util.List; import java.util.Set; @@ -60,12 +61,13 @@ import org.pgpainless.key.info.KeyRingInfo; import org.pgpainless.key.protection.SecretKeyRingProtector; import org.pgpainless.key.protection.UnlockSecretKey; import org.pgpainless.key.util.KeyIdUtil; +import org.pgpainless.key.util.KeyRingUtils; import org.pgpainless.policy.Policy; import org.pgpainless.signature.SignatureUtils; +import org.pgpainless.signature.consumer.CertificateValidator; import org.pgpainless.signature.consumer.OnePassSignatureCheck; import org.pgpainless.signature.consumer.SignatureCheck; import org.pgpainless.signature.consumer.SignatureValidator; -import org.pgpainless.signature.consumer.SignatureVerifier; import org.pgpainless.util.ArmoredInputStreamFactory; import org.pgpainless.util.Passphrase; import org.pgpainless.util.SessionKey; @@ -654,7 +656,16 @@ public class OpenPgpMessageInputStream extends DecryptionStream { if (decryptionKey == null) { continue; } - return secretKeys; + + KeyRingInfo info = new KeyRingInfo(secretKeys, policy, new Date()); + List encryptionKeys = info.getEncryptionSubkeys(EncryptionPurpose.ANY); + for (PGPPublicKey key : encryptionKeys) { + if (key.getKeyID() == keyID) { + return secretKeys; + } + } + + LOGGER.debug("Subkey " + Long.toHexString(keyID) + " cannot be used for decryption."); } return null; } @@ -951,8 +962,8 @@ public class OpenPgpMessageInputStream extends DecryptionStream { try { SignatureValidator.signatureWasCreatedInBounds(options.getVerifyNotBefore(), options.getVerifyNotAfter()) - .verify(signature); - SignatureVerifier.verifyOnePassSignature(signature, onePassSignature.getVerificationKeys().getPublicKey(signature.getKeyID()), onePassSignature, policy); + .verify(signature); + CertificateValidator.validateCertificateAndVerifyOnePassSignature(onePassSignature, policy); LOGGER.debug("Acceptable signature by key " + verification.getSigningKey()); layer.addVerifiedOnePassSignature(verification); } catch (SignatureValidationException e) { @@ -1068,10 +1079,8 @@ public class OpenPgpMessageInputStream extends DecryptionStream { try { SignatureValidator.signatureWasCreatedInBounds(options.getVerifyNotBefore(), options.getVerifyNotAfter()) .verify(detached.getSignature()); - SignatureVerifier.verifyInitializedSignature( - detached.getSignature(), - detached.getSigningKeyRing().getPublicKey(detached.getSigningKeyIdentifier().getKeyId()), - policy, detached.getSignature().getCreationTime()); + CertificateValidator.validateCertificateAndVerifyInitializedSignature( + detached.getSignature(), KeyRingUtils.publicKeys(detached.getSigningKeyRing()), policy); LOGGER.debug("Acceptable signature by key " + verification.getSigningKey()); layer.addVerifiedDetachedSignature(verification); } catch (SignatureValidationException e) { @@ -1085,10 +1094,8 @@ public class OpenPgpMessageInputStream extends DecryptionStream { try { SignatureValidator.signatureWasCreatedInBounds(options.getVerifyNotBefore(), options.getVerifyNotAfter()) .verify(prepended.getSignature()); - SignatureVerifier.verifyInitializedSignature( - prepended.getSignature(), - prepended.getSigningKeyRing().getPublicKey(prepended.getSigningKeyIdentifier().getKeyId()), - policy, prepended.getSignature().getCreationTime()); + CertificateValidator.validateCertificateAndVerifyInitializedSignature( + prepended.getSignature(), KeyRingUtils.publicKeys(prepended.getSigningKeyRing()), policy); LOGGER.debug("Acceptable signature by key " + verification.getSigningKey()); layer.addVerifiedPrependedSignature(verification); } catch (SignatureValidationException e) { diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/util/KeyRingUtils.java b/pgpainless-core/src/main/java/org/pgpainless/key/util/KeyRingUtils.java index 013e283e..2ce058fd 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/key/util/KeyRingUtils.java +++ b/pgpainless-core/src/main/java/org/pgpainless/key/util/KeyRingUtils.java @@ -144,6 +144,17 @@ public final class KeyRingUtils { return secretKey; } + @Nonnull + public static PGPPublicKeyRing publicKeys(@Nonnull PGPKeyRing keys) { + if (keys instanceof PGPPublicKeyRing) { + return (PGPPublicKeyRing) keys; + } else if (keys instanceof PGPSecretKeyRing) { + return publicKeyRingFrom((PGPSecretKeyRing) keys); + } else { + throw new IllegalArgumentException("Unknown keys class: " + keys.getClass().getName()); + } + } + /** * Extract a {@link PGPPublicKeyRing} containing all public keys from the provided {@link PGPSecretKeyRing}. * From a013ab4ebbc3803dcf513897b1cabb1d8620b4d0 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 24 Oct 2022 18:32:56 +0200 Subject: [PATCH 065/763] Wrap MalformedOpenPgpMessageException in BadData --- .../decryption_verification/OpenPgpMessageInputStream.java | 2 +- .../src/main/java/org/pgpainless/sop/DecryptImpl.java | 3 ++- .../src/main/java/org/pgpainless/sop/DetachedVerifyImpl.java | 3 ++- .../src/main/java/org/pgpainless/sop/InlineVerifyImpl.java | 3 ++- 4 files changed, 7 insertions(+), 4 deletions(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java index 11a61e8c..f081e021 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java @@ -656,7 +656,7 @@ public class OpenPgpMessageInputStream extends DecryptionStream { if (decryptionKey == null) { continue; } - + KeyRingInfo info = new KeyRingInfo(secretKeys, policy, new Date()); List encryptionKeys = info.getEncryptionSubkeys(EncryptionPurpose.ANY); for (PGPPublicKey key : encryptionKeys) { diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/DecryptImpl.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/DecryptImpl.java index 11b20f82..ee86f5e8 100644 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/DecryptImpl.java +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/DecryptImpl.java @@ -23,6 +23,7 @@ import org.pgpainless.decryption_verification.ConsumerOptions; import org.pgpainless.decryption_verification.DecryptionStream; import org.pgpainless.decryption_verification.OpenPgpMetadata; import org.pgpainless.decryption_verification.SignatureVerification; +import org.pgpainless.exception.MalformedOpenPgpMessageException; import org.pgpainless.exception.MissingDecryptionMethodException; import org.pgpainless.exception.WrongPassphraseException; import org.pgpainless.util.Passphrase; @@ -123,7 +124,7 @@ public class DecryptImpl implements Decrypt { throw new SOPGPException.CannotDecrypt("No usable decryption key or password provided.", e); } catch (WrongPassphraseException e) { throw new SOPGPException.KeyIsProtected(); - } catch (PGPException | IOException e) { + } catch (MalformedOpenPgpMessageException | PGPException | IOException e) { throw new SOPGPException.BadData(e); } finally { // Forget passphrases after decryption diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/DetachedVerifyImpl.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/DetachedVerifyImpl.java index 3065addf..b563e704 100644 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/DetachedVerifyImpl.java +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/DetachedVerifyImpl.java @@ -18,6 +18,7 @@ import org.pgpainless.decryption_verification.ConsumerOptions; import org.pgpainless.decryption_verification.DecryptionStream; import org.pgpainless.decryption_verification.OpenPgpMetadata; import org.pgpainless.decryption_verification.SignatureVerification; +import org.pgpainless.exception.MalformedOpenPgpMessageException; import sop.Verification; import sop.exception.SOPGPException; import sop.operation.DetachedVerify; @@ -82,7 +83,7 @@ public class DetachedVerifyImpl implements DetachedVerify { } return verificationList; - } catch (PGPException e) { + } catch (MalformedOpenPgpMessageException | PGPException e) { throw new SOPGPException.BadData(e); } } diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/InlineVerifyImpl.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/InlineVerifyImpl.java index 81d614cf..4948712c 100644 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/InlineVerifyImpl.java +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/InlineVerifyImpl.java @@ -19,6 +19,7 @@ import org.pgpainless.decryption_verification.ConsumerOptions; import org.pgpainless.decryption_verification.DecryptionStream; import org.pgpainless.decryption_verification.OpenPgpMetadata; import org.pgpainless.decryption_verification.SignatureVerification; +import org.pgpainless.exception.MalformedOpenPgpMessageException; import org.pgpainless.exception.MissingDecryptionMethodException; import sop.ReadyWithResult; import sop.Verification; @@ -82,7 +83,7 @@ public class InlineVerifyImpl implements InlineVerify { return verificationList; } catch (MissingDecryptionMethodException e) { throw new SOPGPException.BadData("Cannot verify encrypted message.", e); - } catch (PGPException e) { + } catch (MalformedOpenPgpMessageException | PGPException e) { throw new SOPGPException.BadData(e); } } From 192aa9832621ec759f08656681d7a0c6340f8a63 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 24 Oct 2022 18:38:30 +0200 Subject: [PATCH 066/763] Add missing REUSE license headers --- .../pgpainless/decryption_verification/DecryptionStream.java | 4 ++++ .../OpenPgpMessageInputStreamTest.java | 4 ++++ .../decryption_verification/syntax_check/PDATest.java | 4 ++++ 3 files changed, 12 insertions(+) diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionStream.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionStream.java index c531f487..0f221ad7 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionStream.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionStream.java @@ -1,3 +1,7 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + package org.pgpainless.decryption_verification; import javax.annotation.Nonnull; diff --git a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStreamTest.java b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStreamTest.java index f9539149..d2c62fb2 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStreamTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStreamTest.java @@ -1,3 +1,7 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + package org.pgpainless.decryption_verification; import static org.junit.jupiter.api.Assertions.assertEquals; diff --git a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/syntax_check/PDATest.java b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/syntax_check/PDATest.java index 8fa107f6..9250acfa 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/syntax_check/PDATest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/syntax_check/PDATest.java @@ -1,3 +1,7 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + package org.pgpainless.decryption_verification.syntax_check; import static org.junit.jupiter.api.Assertions.assertThrows; From 7e8841abf3097a569ff4e9e5285c26adf635b4b0 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 24 Oct 2022 19:23:52 +0200 Subject: [PATCH 067/763] Handle unknown packet versions gracefully --- .../OpenPgpMessageInputStream.java | 9 +- .../UnsupportedPacketVersionsTest.java | 410 ++++++++++++++++++ 2 files changed, 418 insertions(+), 1 deletion(-) create mode 100644 pgpainless-core/src/test/java/org/pgpainless/decryption_verification/UnsupportedPacketVersionsTest.java diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java index f081e021..75b78294 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java @@ -18,6 +18,7 @@ import javax.annotation.Nonnull; import org.bouncycastle.bcpg.ArmoredInputStream; import org.bouncycastle.bcpg.BCPGInputStream; +import org.bouncycastle.bcpg.UnsupportedPacketVersionException; import org.bouncycastle.openpgp.PGPCompressedData; import org.bouncycastle.openpgp.PGPEncryptedData; import org.bouncycastle.openpgp.PGPEncryptedDataList; @@ -366,7 +367,13 @@ public class OpenPgpMessageInputStream extends DecryptionStream { // true if Signature corresponds to OnePassSignature boolean isSigForOPS = syntaxVerifier.peekStack() == StackAlphabet.ops; syntaxVerifier.next(InputAlphabet.Signature); - PGPSignature signature = packetInputStream.readSignature(); + PGPSignature signature; + try { + signature = packetInputStream.readSignature(); + } catch (UnsupportedPacketVersionException e) { + LOGGER.debug("Unsupported Signature at depth " + metadata.depth + " encountered.", e); + return; + } long keyId = SignatureUtils.determineIssuerKeyId(signature); if (isSigForOPS) { LOGGER.debug("Signature Packet corresponding to One-Pass-Signature by key " + diff --git a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/UnsupportedPacketVersionsTest.java b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/UnsupportedPacketVersionsTest.java new file mode 100644 index 00000000..68916348 --- /dev/null +++ b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/UnsupportedPacketVersionsTest.java @@ -0,0 +1,410 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.decryption_verification; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.nio.charset.Charset; + +import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.PGPPublicKeyRing; +import org.bouncycastle.openpgp.PGPSecretKeyRing; +import org.bouncycastle.util.io.Streams; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.pgpainless.PGPainless; + +public class UnsupportedPacketVersionsTest { + + private static final String KEY = "-----BEGIN PGP PRIVATE KEY BLOCK-----\n" + + "Comment: Bob's OpenPGP Transferable Secret Key\n" + + "\n" + + "lQVYBF2lnPIBDAC5cL9PQoQLTMuhjbYvb4Ncuuo0bfmgPRFywX53jPhoFf4Zg6mv\n" + + "/seOXpgecTdOcVttfzC8ycIKrt3aQTiwOG/ctaR4Bk/t6ayNFfdUNxHWk4WCKzdz\n" + + "/56fW2O0F23qIRd8UUJp5IIlN4RDdRCtdhVQIAuzvp2oVy/LaS2kxQoKvph/5pQ/\n" + + "5whqsyroEWDJoSV0yOb25B/iwk/pLUFoyhDG9bj0kIzDxrEqW+7Ba8nocQlecMF3\n" + + "X5KMN5kp2zraLv9dlBBpWW43XktjcCZgMy20SouraVma8Je/ECwUWYUiAZxLIlMv\n" + + "9CurEOtxUw6N3RdOtLmYZS9uEnn5y1UkF88o8Nku890uk6BrewFzJyLAx5wRZ4F0\n" + + "qV/yq36UWQ0JB/AUGhHVPdFf6pl6eaxBwT5GXvbBUibtf8YI2og5RsgTWtXfU7eb\n" + + "SGXrl5ZMpbA6mbfhd0R8aPxWfmDWiIOhBufhMCvUHh1sApMKVZnvIff9/0Dca3wb\n" + + "vLIwa3T4CyshfT0AEQEAAQAL/RZqbJW2IqQDCnJi4Ozm++gPqBPiX1RhTWSjwxfM\n" + + "cJKUZfzLj414rMKm6Jh1cwwGY9jekROhB9WmwaaKT8HtcIgrZNAlYzANGRCM4TLK\n" + + "3VskxfSwKKna8l+s+mZglqbAjUg3wmFuf9Tj2xcUZYmyRm1DEmcN2ZzpvRtHgX7z\n" + + "Wn1mAKUlSDJZSQks0zjuMNbupcpyJokdlkUg2+wBznBOTKzgMxVNC9b2g5/tMPUs\n" + + "hGGWmF1UH+7AHMTaS6dlmr2ZBIyogdnfUqdNg5sZwsxSNrbglKP4sqe7X61uEAIQ\n" + + "bD7rT3LonLbhkrj3I8wilUD8usIwt5IecoHhd9HziqZjRCc1BUBkboUEoyedbDV4\n" + + "i4qfsFZ6CEWoLuD5pW7dEp0M+WeuHXO164Rc+LnH6i1VQrpb1Okl4qO6ejIpIjBI\n" + + "1t3GshtUu/mwGBBxs60KBX5g77mFQ9lLCRj8lSYqOsHRKBhUp4qM869VA+fD0BRP\n" + + "fqPT0I9IH4Oa/A3jYJcg622GwQYA1LhnP208Waf6PkQSJ6kyr8ymY1yVh9VBE/g6\n" + + "fRDYA+pkqKnw9wfH2Qho3ysAA+OmVOX8Hldg+Pc0Zs0e5pCavb0En8iFLvTA0Q2E\n" + + "LR5rLue9uD7aFuKFU/VdcddY9Ww/vo4k5p/tVGp7F8RYCFn9rSjIWbfvvZi1q5Tx\n" + + "+akoZbga+4qQ4WYzB/obdX6SCmi6BndcQ1QdjCCQU6gpYx0MddVERbIp9+2SXDyL\n" + + "hpxjSyz+RGsZi/9UAshT4txP4+MZBgDfK3ZqtW+h2/eMRxkANqOJpxSjMyLO/FXN\n" + + "WxzTDYeWtHNYiAlOwlQZEPOydZFty9IVzzNFQCIUCGjQ/nNyhw7adSgUk3+BXEx/\n" + + "MyJPYY0BYuhLxLYcrfQ9nrhaVKxRJj25SVHj2ASsiwGJRZW4CC3uw40OYxfKEvNC\n" + + "mer/VxM3kg8qqGf9KUzJ1dVdAvjyx2Hz6jY2qWCyRQ6IMjWHyd43C4r3jxooYKUC\n" + + "YnstRQyb/gCSKahveSEjo07CiXMr88UGALwzEr3npFAsPW3osGaFLj49y1oRe11E\n" + + "he9gCHFm+fuzbXrWmdPjYU5/ZdqdojzDqfu4ThfnipknpVUM1o6MQqkjM896FHm8\n" + + "zbKVFSMhEP6DPHSCexMFrrSgN03PdwHTO6iBaIBBFqmGY01tmJ03SxvSpiBPON9P\n" + + "NVvy/6UZFedTq8A07OUAxO62YUSNtT5pmK2vzs3SAZJmbFbMh+NN204TRI72GlqT\n" + + "t5hcfkuv8hrmwPS/ZR6q312mKQ6w/1pqO9qitCFCb2IgQmFiYmFnZSA8Ym9iQG9w\n" + + "ZW5wZ3AuZXhhbXBsZT6JAc4EEwEKADgCGwMFCwkIBwIGFQoJCAsCBBYCAwECHgEC\n" + + "F4AWIQTRpm4aI7GCyZgPeIz7/MgqAV5zMAUCXaWe+gAKCRD7/MgqAV5zMG9sC/9U\n" + + "2T3RrqEbw533FPNfEflhEVRIZ8gDXKM8hU6cqqEzCmzZT6xYTe6sv4y+PJBGXJFX\n" + + "yhj0g6FDkSyboM5litOcTupURObVqMgA/Y4UKERznm4fzzH9qek85c4ljtLyNufe\n" + + "doL2pp3vkGtn7eD0QFRaLLmnxPKQ/TlZKdLE1G3u8Uot8QHicaR6GnAdc5UXQJE3\n" + + "BiV7jZuDyWmZ1cUNwJkKL6oRtp+ZNDOQCrLNLecKHcgCqrpjSQG5oouba1I1Q6Vl\n" + + "sP44dhA1nkmLHtxlTOzpeHj4jnk1FaXmyasurrrI5CgU/L2Oi39DGKTH/A/cywDN\n" + + "4ZplIQ9zR8enkbXquUZvFDe+Xz+6xRXtb5MwQyWODB3nHw85HocLwRoIN9WdQEI+\n" + + "L8a/56AuOwhs8llkSuiITjR7r9SgKJC2WlAHl7E8lhJ3VDW3ELC56KH308d6mwOG\n" + + "ZRAqIAKzM1T5FGjMBhq7ZV0eqdEntBh3EcOIfj2M8rg1MzJv+0mHZOIjByawikad\n" + + "BVgEXaWc8gEMANYwv1xsYyunXYK0X1vY/rP1NNPvhLyLIE7NpK90YNBj+xS1ldGD\n" + + "bUdZqZeef2xJe8gMQg05DoD1DF3GipZ0Ies65beh+d5hegb7N4pzh0LzrBrVNHar\n" + + "29b5ExdI7i4iYD5TO6Vr/qTUOiAN/byqELEzAb+L+b2DVz/RoCm4PIp1DU9ewcc2\n" + + "WB38Ofqut3nLYA5tqJ9XvAiEQme+qAVcM3ZFcaMt4I4dXhDZZNg+D9LiTWcxdUPB\n" + + "leu8iwDRjAgyAhPzpFp+nWoqWA81uIiULWD1Fj+IVoY3ZvgivoYOiEFBJ9lbb4te\n" + + "g9m5UT/AaVDTWuHzbspVlbiVe+qyB77C2daWzNyx6UYBPLOo4r0t0c91kbNE5lgj\n" + + "Z7xz6los0N1U8vq91EFSeQJoSQ62XWavYmlCLmdNT6BNfgh4icLsT7Vr1QMX9jzn\n" + + "JtTPxdXytSdHvpSpULsqJ016l0dtmONcK3z9mj5N5z0k1tg1AH970TGYOe2aUcSx\n" + + "IRDMXDOPyzEfjwARAQABAAv9F2CwsjS+Sjh1M1vegJbZjei4gF1HHpEM0K0PSXsp\n" + + "SfVvpR4AoSJ4He6CXSMWg0ot8XKtDuZoV9jnJaES5UL9pMAD7JwIOqZm/DYVJM5h\n" + + "OASCh1c356/wSbFbzRHPtUdZO9Q30WFNJM5pHbCJPjtNoRmRGkf71RxtvHBzy7np\n" + + "Ga+W6U/NVKHw0i0CYwMI0YlKDakYW3Pm+QL+gHZFvngGweTod0f9l2VLLAmeQR/c\n" + + "+EZs7lNumhuZ8mXcwhUc9JQIhOkpO+wreDysEFkAcsKbkQP3UDUsA1gFx9pbMzT0\n" + + "tr1oZq2a4QBtxShHzP/ph7KLpN+6qtjks3xB/yjTgaGmtrwM8tSe0wD1RwXS+/1o\n" + + "BHpXTnQ7TfeOGUAu4KCoOQLv6ELpKWbRBLWuiPwMdbGpvVFALO8+kvKAg9/r+/ny\n" + + "zM2GQHY+J3Jh5JxPiJnHfXNZjIKLbFbIPdSKNyJBuazXW8xIa//mEHMI5OcvsZBK\n" + + "clAIp7LXzjEjKXIwHwDcTn9pBgDpdOKTHOtJ3JUKx0rWVsDH6wq6iKV/FTVSY5jl\n" + + "zN+puOEsskF1Lfxn9JsJihAVO3yNsp6RvkKtyNlFazaCVKtDAmkjoh60XNxcNRqr\n" + + "gCnwdpbgdHP6v/hvZY54ZaJjz6L2e8unNEkYLxDt8cmAyGPgH2XgL7giHIp9jrsQ\n" + + "aS381gnYwNX6wE1aEikgtY91nqJjwPlibF9avSyYQoMtEqM/1UjTjB2KdD/MitK5\n" + + "fP0VpvuXpNYZedmyq4UOMwdkiNMGAOrfmOeT0olgLrTMT5H97Cn3Yxbk13uXHNu/\n" + + "ZUZZNe8s+QtuLfUlKAJtLEUutN33TlWQY522FV0m17S+b80xJib3yZVJteVurrh5\n" + + "HSWHAM+zghQAvCesg5CLXa2dNMkTCmZKgCBvfDLZuZbjFwnwCI6u/NhOY9egKuUf\n" + + "SA/je/RXaT8m5VxLYMxwqQXKApzD87fv0tLPlVIEvjEsaf992tFEFSNPcG1l/jpd\n" + + "5AVXw6kKuf85UkJtYR1x2MkQDrqY1QX/XMw00kt8y9kMZUre19aCArcmor+hDhRJ\n" + + "E3Gt4QJrD9z/bICESw4b4z2DbgD/Xz9IXsA/r9cKiM1h5QMtXvuhyfVeM01enhxM\n" + + "GbOH3gjqqGNKysx0UODGEwr6AV9hAd8RWXMchJLaExK9J5SRawSg671ObAU24SdY\n" + + "vMQ9Z4kAQ2+1ReUZzf3ogSMRZtMT+d18gT6L90/y+APZIaoArLPhebIAGq39HLmJ\n" + + "26x3z0WAgrpA1kNsjXEXkoiZGPLKIGoe3hqJAbYEGAEKACAWIQTRpm4aI7GCyZgP\n" + + "eIz7/MgqAV5zMAUCXaWc8gIbDAAKCRD7/MgqAV5zMOn/C/9ugt+HZIwX308zI+QX\n" + + "c5vDLReuzmJ3ieE0DMO/uNSC+K1XEioSIZP91HeZJ2kbT9nn9fuReuoff0T0Dief\n" + + "rbwcIQQHFFkrqSp1K3VWmUGp2JrUsXFVdjy/fkBIjTd7c5boWljv/6wAsSfiv2V0\n" + + "JSM8EFU6TYXxswGjFVfc6X97tJNeIrXL+mpSmPPqy2bztcCCHkWS5lNLWQw+R7Vg\n" + + "71Fe6yBSNVrqC2/imYG2J9zlowjx1XU63Wdgqp2Wxt0l8OmsB/W80S1fRF5G4SDH\n" + + "s9HXglXXqPsBRZJYfP+VStm9L5P/sKjCcX6WtZR7yS6G8zj/X767MLK/djANvpPd\n" + + "NVniEke6hM3CNBXYPAMhQBMWhCulcoz+0lxi8L34rMN+Dsbma96psdUrn7uLaB91\n" + + "6we0CTfF8qqm7BsVAgalon/UUiuMY80U3ueoj3okiSTiHIjD/YtpXSPioC8nMng7\n" + + "xqAY9Bwizt4FWgXuLm1a4+So4V9j1TRCXd12Uc2l2RNmgDE=\n" + + "=miES\n" + + "-----END PGP PRIVATE KEY BLOCK-----\n"; + + private static final String PKESK3_PKESK23_SEIP = "-----BEGIN PGP MESSAGE-----\n" + + "\n" + + "wcDMA3wvqk35PDeyAQv/YP+fDWtifT7KSk+tWrgbyvsYCt5Wh0IPESTZuiptwvto\n" + + "CGbOfwuPbqqzzlFqSvX3UiJwhxjSSB3a1EBIOsbhc4grip/wm+fB50S/nTJxkJ14\n" + + "qid40D7HOcIvuz1iQr1QoMNB0oT3nCwMec8mPUX2yOzx1eqr62SZUTCr6FdAmdYI\n" + + "1u4EAeEFhRO0rcPRrpMZqwkXtUfx+pu7OzBS0qmOlfkQ50kbETDXBik4iXi30AGl\n" + + "Ifo792oRo6DFK7ENquTNRqFPfezjrGZfkJrPWulWh28GogWTpOBwfXG8X262QoIp\n" + + "VwZygi7wfj1jh2sXPvWgHjsjjTt7HPAiLI1f6IUl8WCQfPuQkFwCwPv63/rve59v\n" + + "sBaeCEykAxdzMbP1oYSBBtONSAPYW9fsUsJSpuuLvxH252+luk09uQXWd6z4aCDm\n" + + "EXiolhbkzL3mXCpVP6nMjRkm2ERE1yAWgXGT9JON0gcCb3eVqw6wzOYu+Vwq70ND\n" + + "vKYlTMY+9RUx7wLn51UgwUoXQUFBQUFBQUEJYWFhYWFhYWFhYWFhYWFhYWFhYWFh\n" + + "YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYdJMAQZo\n" + + "tUFmYcTQrd5IFriHbF8h/Ov/xKlkW1QOPrZ+ziMQRbwyY4pVwNbZjCNVCHg5QDO4\n" + + "wjF686DfZt83NvVYbJ7QNuENoI4YcFj8nw==\n" + + "=VS1M\n" + + "-----END PGP MESSAGE-----"; + + private static final String PKESK23_PKESK3_SEIP = "-----BEGIN PGP MESSAGE-----\n" + + "\n" + + "wUoXQUFBQUFBQUEJYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFh\n" + + "YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYcHAzAN8L6pN+Tw3sgEL/2D/nw1r\n" + + "Yn0+ykpPrVq4G8r7GAreVodCDxEk2boqbcL7aAhmzn8Lj26qs85Rakr191IicIcY\n" + + "0kgd2tRASDrG4XOIK4qf8JvnwedEv50ycZCdeKoneNA+xznCL7s9YkK9UKDDQdKE\n" + + "95wsDHnPJj1F9sjs8dXqq+tkmVEwq+hXQJnWCNbuBAHhBYUTtK3D0a6TGasJF7VH\n" + + "8fqbuzswUtKpjpX5EOdJGxEw1wYpOIl4t9ABpSH6O/dqEaOgxSuxDarkzUahT33s\n" + + "46xmX5Caz1rpVodvBqIFk6TgcH1xvF9utkKCKVcGcoIu8H49Y4drFz71oB47I407\n" + + "exzwIiyNX+iFJfFgkHz7kJBcAsD7+t/673ufb7AWnghMpAMXczGz9aGEgQbTjUgD\n" + + "2FvX7FLCUqbri78R9udvpbpNPbkF1nes+Ggg5hF4qJYW5My95lwqVT+pzI0ZJthE\n" + + "RNcgFoFxk/STjdIHAm93lasOsMzmLvlcKu9DQ7ymJUzGPvUVMe8C5+dVINJMAQZo\n" + + "tUFmYcTQrd5IFriHbF8h/Ov/xKlkW1QOPrZ+ziMQRbwyY4pVwNbZjCNVCHg5QDO4\n" + + "wjF686DfZt83NvVYbJ7QNuENoI4YcFj8nw==\n" + + "=EhNy\n" + + "-----END PGP MESSAGE-----\n"; + + private static final String PKESK3_SKESK23_SEIP = "-----BEGIN PGP MESSAGE-----\n" + + "\n" + + "wcDMA3wvqk35PDeyAQv/YP+fDWtifT7KSk+tWrgbyvsYCt5Wh0IPESTZuiptwvto\n" + + "CGbOfwuPbqqzzlFqSvX3UiJwhxjSSB3a1EBIOsbhc4grip/wm+fB50S/nTJxkJ14\n" + + "qid40D7HOcIvuz1iQr1QoMNB0oT3nCwMec8mPUX2yOzx1eqr62SZUTCr6FdAmdYI\n" + + "1u4EAeEFhRO0rcPRrpMZqwkXtUfx+pu7OzBS0qmOlfkQ50kbETDXBik4iXi30AGl\n" + + "Ifo792oRo6DFK7ENquTNRqFPfezjrGZfkJrPWulWh28GogWTpOBwfXG8X262QoIp\n" + + "VwZygi7wfj1jh2sXPvWgHjsjjTt7HPAiLI1f6IUl8WCQfPuQkFwCwPv63/rve59v\n" + + "sBaeCEykAxdzMbP1oYSBBtONSAPYW9fsUsJSpuuLvxH252+luk09uQXWd6z4aCDm\n" + + "EXiolhbkzL3mXCpVP6nMjRkm2ERE1yAWgXGT9JON0gcCb3eVqw6wzOYu+Vwq70ND\n" + + "vKYlTMY+9RUx7wLn51Ugw00XCQMINQp7MFzAc6T/YWFhYWFhYWFhYWFhYWFhYWFh\n" + + "YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYdJM\n" + + "AQZotUFmYcTQrd5IFriHbF8h/Ov/xKlkW1QOPrZ+ziMQRbwyY4pVwNbZjCNVCHg5\n" + + "QDO4wjF686DfZt83NvVYbJ7QNuENoI4YcFj8nw==\n" + + "=pvWj\n" + + "-----END PGP MESSAGE-----\n"; + + private static final String SKESK23_PKESK3_SEIP = "-----BEGIN PGP MESSAGE-----\n" + + "\n" + + "w00XCQMINQp7MFzAc6T/YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFh\n" + + "YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYcHAzAN8L6pN+Tw3sgEL/2D/\n" + + "nw1rYn0+ykpPrVq4G8r7GAreVodCDxEk2boqbcL7aAhmzn8Lj26qs85Rakr191Ii\n" + + "cIcY0kgd2tRASDrG4XOIK4qf8JvnwedEv50ycZCdeKoneNA+xznCL7s9YkK9UKDD\n" + + "QdKE95wsDHnPJj1F9sjs8dXqq+tkmVEwq+hXQJnWCNbuBAHhBYUTtK3D0a6TGasJ\n" + + "F7VH8fqbuzswUtKpjpX5EOdJGxEw1wYpOIl4t9ABpSH6O/dqEaOgxSuxDarkzUah\n" + + "T33s46xmX5Caz1rpVodvBqIFk6TgcH1xvF9utkKCKVcGcoIu8H49Y4drFz71oB47\n" + + "I407exzwIiyNX+iFJfFgkHz7kJBcAsD7+t/673ufb7AWnghMpAMXczGz9aGEgQbT\n" + + "jUgD2FvX7FLCUqbri78R9udvpbpNPbkF1nes+Ggg5hF4qJYW5My95lwqVT+pzI0Z\n" + + "JthERNcgFoFxk/STjdIHAm93lasOsMzmLvlcKu9DQ7ymJUzGPvUVMe8C5+dVINJM\n" + + "AQZotUFmYcTQrd5IFriHbF8h/Ov/xKlkW1QOPrZ+ziMQRbwyY4pVwNbZjCNVCHg5\n" + + "QDO4wjF686DfZt83NvVYbJ7QNuENoI4YcFj8nw==\n" + + "=STOd\n" + + "-----END PGP MESSAGE-----\n"; + + private static final String PKESK3_SKESK4wS2K23_SEIP = "-----BEGIN PGP MESSAGE-----\n" + + "\n" + + "wcDMA3wvqk35PDeyAQv/YP+fDWtifT7KSk+tWrgbyvsYCt5Wh0IPESTZuiptwvto\n" + + "CGbOfwuPbqqzzlFqSvX3UiJwhxjSSB3a1EBIOsbhc4grip/wm+fB50S/nTJxkJ14\n" + + "qid40D7HOcIvuz1iQr1QoMNB0oT3nCwMec8mPUX2yOzx1eqr62SZUTCr6FdAmdYI\n" + + "1u4EAeEFhRO0rcPRrpMZqwkXtUfx+pu7OzBS0qmOlfkQ50kbETDXBik4iXi30AGl\n" + + "Ifo792oRo6DFK7ENquTNRqFPfezjrGZfkJrPWulWh28GogWTpOBwfXG8X262QoIp\n" + + "VwZygi7wfj1jh2sXPvWgHjsjjTt7HPAiLI1f6IUl8WCQfPuQkFwCwPv63/rve59v\n" + + "sBaeCEykAxdzMbP1oYSBBtONSAPYW9fsUsJSpuuLvxH252+luk09uQXWd6z4aCDm\n" + + "EXiolhbkzL3mXCpVP6nMjRkm2ERE1yAWgXGT9JON0gcCb3eVqw6wzOYu+Vwq70ND\n" + + "vKYlTMY+9RUx7wLn51Ugw1AECRcIYWFhYWFhYWFBQUFBYWFhYWFhYWFhYWFhYWFh\n" + + "YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFh\n" + + "YdJMAQZotUFmYcTQrd5IFriHbF8h/Ov/xKlkW1QOPrZ+ziMQRbwyY4pVwNbZjCNV\n" + + "CHg5QDO4wjF686DfZt83NvVYbJ7QNuENoI4YcFj8nw==\n" + + "=/uxY\n" + + "-----END PGP MESSAGE-----\n"; + + private static final String SKESK4wS2K23_PKESK3_SEIP = "-----BEGIN PGP MESSAGE-----\n" + + "\n" + + "w1AECRcIYWFhYWFhYWFBQUFBYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFh\n" + + "YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYcHAzAN8L6pN+Tw3sgEL\n" + + "/2D/nw1rYn0+ykpPrVq4G8r7GAreVodCDxEk2boqbcL7aAhmzn8Lj26qs85Rakr1\n" + + "91IicIcY0kgd2tRASDrG4XOIK4qf8JvnwedEv50ycZCdeKoneNA+xznCL7s9YkK9\n" + + "UKDDQdKE95wsDHnPJj1F9sjs8dXqq+tkmVEwq+hXQJnWCNbuBAHhBYUTtK3D0a6T\n" + + "GasJF7VH8fqbuzswUtKpjpX5EOdJGxEw1wYpOIl4t9ABpSH6O/dqEaOgxSuxDark\n" + + "zUahT33s46xmX5Caz1rpVodvBqIFk6TgcH1xvF9utkKCKVcGcoIu8H49Y4drFz71\n" + + "oB47I407exzwIiyNX+iFJfFgkHz7kJBcAsD7+t/673ufb7AWnghMpAMXczGz9aGE\n" + + "gQbTjUgD2FvX7FLCUqbri78R9udvpbpNPbkF1nes+Ggg5hF4qJYW5My95lwqVT+p\n" + + "zI0ZJthERNcgFoFxk/STjdIHAm93lasOsMzmLvlcKu9DQ7ymJUzGPvUVMe8C5+dV\n" + + "INJMAQZotUFmYcTQrd5IFriHbF8h/Ov/xKlkW1QOPrZ+ziMQRbwyY4pVwNbZjCNV\n" + + "CHg5QDO4wjF686DfZt83NvVYbJ7QNuENoI4YcFj8nw==\n" + + "=cIwV\n" + + "-----END PGP MESSAGE-----\n"; + + private static final String PKESK3_SEIP_OPS3_LIT_SIG4 = "-----BEGIN PGP MESSAGE-----\n" + + "\n" + + "wcDMA3wvqk35PDeyAQv+IhkCMhbdRcMnIPZNPGU6OK1Jk5xuRdIEIBsvv7b8jmAr\n" + + "9IwjfnV/RDMtH+xR/T9K7qJGGFYnhLY5w0CmYHQcDKpcBqk0Dw6l/eKCNhgRXKAk\n" + + "gfaKL1Utt1Pw0nz0mOwHyPEN/pGc0xlVhsjVkRvIOsKcpfuc1EpSZMFgDcBQDhe/\n" + + "jAsR/MvRugkW8xLpyQyfeGLJUOEYVrkpam3rLKB1KywAgBmpr9WDwfYITW/VE9k5\n" + + "cKIOPMDJFU+u9lzBx6nSS4JRBuCO2mhR4gjcRaGPWiiz+0qZfS+AXYV/MAU5OwK/\n" + + "6nhX97zwS4r1Avztjh4taBhLVsY4pw6PuLtACJNwPrev63Yc+a4hJJ4pm1AnHV58\n" + + "Y1pZKQL8vA61+/tbhFQ18vbJ8E1NOka/euFLQu9Mg58jhpcscqMouyr3JFMwgH2Y\n" + + "eFuRJncJAKotXxlfnF37qz5LG3bamACXWZSObjp9d4quIAoCDUteZlDWQ1xq5R4y\n" + + "QYXtk9ZuHsHsmY9A0CiI0sGYAUBtLubJ5qhLLr/GqKAmy8jTSA3MjgtrB55NWj9J\n" + + "bjgFNsd1BNGklgnwhtmApHJWY8skAAQkJj0rXj/aOMc734ypiEWDiU1quRbEeRLR\n" + + "kDvBNUXx2j2rVF+MmQS/sm5Yk/op+4lH/Wounsci3qWH76GaNZoIlvNE3mdFoVTe\n" + + "cRh4W2Em8uAUH4bKwazltRJUhZmXvuGfUnQCmolJTpyPl4DaQQgzdBXLTRcPxwdU\n" + + "30e7HnxZWESKx1LnGxp3Oan1k1lXyHwvnEk26EXhve+dhsQ6YsKgvtSLNFqGsfKe\n" + + "MVOq37cpOGFQsYStWHZd0tcHjIjWmAeZ8kH+ZzR9tgYKxxjimsxafLS/lo415SkC\n" + + "LnOCz6hywI7CufSUcXUlHGJuobZ5HDJcygsQhQmNVLDmKh4xUJDZrORS0ciMy2kc\n" + + "XAnxCDYbltVQktc/F/Gl1lTx0UNmV5d9G0utVmxxGbXna3BLkB+6qMuux/ngC+cI\n" + + "+0GjeuXDVzUqhKDDC3Sq4T2nwix4CjKvawnHC+vpHzRmdZSkXz3nrSdhJS5JnOap\n" + + "I4CdyYkP3jQs5P03dkv5RZAoqNPkeftwadu0cjZj8HEC8bVSd7YCS/0Gbgvp/eK3\n" + + "aOXZAWhELyQON4bbXiWzMTcO2AA5soBmP2QnBNdq7NxbEkBec8aAXZiyXn17JYit\n" + + "HRtJ7beptXjN0y2bEhOvsBHbDpjGO22fZfQ0aywP2k4/XanDf4WJolEFgLj2Qp8F\n" + + "pw1Olc2UhApxhAqjkme09hggli3wUhthQrpBlmntfbvBcmmO+p/jV8Zn\n" + + "=Roko\n" + + "-----END PGP MESSAGE-----\n"; + + private static final String PKESK3_SEIP_OPS23_LIT_SIG23 = "-----BEGIN PGP MESSAGE-----\n" + + "\n" + + "wcDMA3wvqk35PDeyAQv/byYGOehnfgZu/HJSSDEQhYE27lh5j+oQPktYBxsTI3Ii\n" + + "AJ0pyJggtUM3c/e6z4HioAs4oQiaH1eoGBIl7noROhHJgT6E/oKdHJHmQndo0gT7\n" + + "xzSHKrEZYEqT45LD6jT5teOiwGX/B7as6jaQOT+Nh+M0ZqpPrBdWDeUoY/I/lx9j\n" + + "IcXQUhKuqJwZ16xsnv0JJ80rdp5qv0g2NHT1hK1JEONyT2fefov3EXaSpQZHRBEi\n" + + "XfwToHcJrgemFoGwZBQhXsPWBgKH92aW6r7ZJZMQ4BE2SwqEw+cbaaqwfFRJ9puj\n" + + "ZUBi2JGwnYImZyD7jYverkjH05vI7d5qQDhCT6GPD8Q0WKfY225LFTj/zzbC23lp\n" + + "VLlbT2Ap8ZFOESpM+crOUaOguBhnTOF05s1eXhQYxIKzJTW8UVrzbwI8ut3BnEDj\n" + + "0aDqUR+QDYgYmz8hnEjanHk2McvfaNdV6uOPZNph2RuPzhZeoNcz1PLy+XPFJsQT\n" + + "EdqURJ2D35qmrBvq6klN0sGYAXEtW4hxMaKhH7/SIk2m3kUAChjtzr4XRcE2i8h3\n" + + "9uD6CRWxokArfRVAp5RpXt3ywoYrl1Mp5prVwWcrTzYVPZwwe/bYAFTIfavi0Ezb\n" + + "A/ah2e1EYCTWxLY0Klzil2Xw9/Dc/JPTRqzWJxIn0AU4DVfYNwlH3QimDUDKOKbu\n" + + "bw4bLEEBKRr5BcNMA2rJOw/n3AmKxQcdSFJh3ZNtDPWzRwflIzE2qB686hpeOs34\n" + + "T2iJcfr9W9rKcYI7+WYcZA3fWokaWUfXdWrPMXBVJuPdGezGLSfe/OM4kw/8s2vR\n" + + "Bk88WZ1ZiFXE2CRaHP80fHFpxAZioWTuC5UGhF7NgZ7Q1E85GaGVe1fQeqmeX3mo\n" + + "gwAWwq9WFhPQQPwdrDz+1h/pzD0RVW7D+zfWdF//vesc4z1Bpi5prbMdpVdmyvO9\n" + + "8Rcc42GtmhtYSB9SPjzPuN8PrWvD3AKgw5vQro6oiNh0TGmj5Se4lXCfRfCl4tak\n" + + "cmvdi+1wAvn5OFdxYKKHNvjavwj2SY70nx0ACasBpbMEwoQ0StZmOxCaofkgvEwN\n" + + "t8jMq/MVrIfunhMjx0/GDpBZBb8kze8zrvWxlbTnoIfh2yVEqmTZWue9HX5Mnh8P\n" + + "wexxNrPaafTjA3nUgXXzlItJ/Wa43pYw2sgcBPlF/jRKSFiD+pDLysqwpH0ANsJ3\n" + + "G7t8Qavq1DlrHFgV5jhfR1tjA5ohjxx7yzceQBvZUFxKM1WEWR+9dRb3bpfZJr0g\n" + + "qgO36bpCeCEej+ubXpXXTN28LQLXjQHlE2o1NGLoGl+G72tXOTx30kPS\n" + + "=wUZn\n" + + "-----END PGP MESSAGE-----\n"; + + private static final String PKESK3_SEIP_OPS23_OPS3_LIT_SIG4_SIG23 = "-----BEGIN PGP MESSAGE-----\n" + + "\n" + + "wcDMA3wvqk35PDeyAQwAmftqzbRiMRk7YbpJO7uqtWQIq2uZj7nYYOVpQgR777Zb\n" + + "QT1J3pbPoCceWFSuHH/D0IeP84T95O+A630dtj3ZWhh4EGnBIKm13R6bEA4jKtcx\n" + + "rK3HZyHo85N5TFn/gLOqCc2v/0cxUoygBqFpHFwBG/e4uuQoxgD4RsBGFrdYlyK7\n" + + "MMkZ/fgaKhawTCVQ2dtBcp6WvKX/8u6FXSh35zFJJTM30LE7BFDgWEyGPLv0bWnl\n" + + "37oCo8zcK+SIK4JooNm1ZxIowWTOxUz1pWPtmtGXA8XoGJtggXt+HpVxNE8bDCgV\n" + + "f717+R0HMwQKP4F50Wq3Js9ZJrPjRvXLeiZe6nvqn5AQCOsG8osIIg2YElyW/uIe\n" + + "C38OxaZbql297cYzzdEZtRYCTTY7j99pG8ZD1nNdd5IPm027dfPl+JJ5nzn/u3iT\n" + + "+FKxgArZ9cYkeEpNB+BfWoWyNfbA830s3Y+G5wt3s4cmH1z8JvDeepUYhyqvA/6M\n" + + "RInjwCUwSQCAuI+QhnPi0sOlAYOwkgN8RyqOP4vR4Kd1rJfm7rg2h40ag7tJtu5y\n" + + "YXa4sU9DLpspTYvYgzRtsNbYHrH+LFkQlpG+PFnJAI6Qn+Q1zZsZ2HbbK7GOZFug\n" + + "0wCg5E3DJQxBNlOx7h7xacpGb2QsmBTLUPvWFiYoq8He5XOx1MleLxO+E5l40kT4\n" + + "ZE8RxfRyVreQdp3rZ6RMiIfnM8VljMxteaLmaIzqsLTlvKlf1w2DBRL9reI2oCP6\n" + + "BukX8zba16ITsLELnuQ5L8EmQwcH8Yj8Mg37foyFHa2fIvZRg+0tz3b5nJKteWQn\n" + + "u/qR/RGNAXT1D/YBQ+Tgnq6qIUd4Da/XEXAi8R6FKprc2yqCNzMSA1wolaq2DUGJ\n" + + "ASyRN7uQJpVc1DlxTRTMQLpuoxljQtc6dmn/HKr7DF8jUKcM8cRk6PCqWd6PRYPq\n" + + "WJTiHh2FoyDaR5+HuSbRCOr7i9jZXh/TctM70itLIvQlw/x9WEm2ZxwS7+0mHMvP\n" + + "h9U4Wi70mfR0WllDNpWm5ZEeksoUF7aCQ2lVIQH8E6YGmWUSCYUgjgiIsfSqn9kh\n" + + "tG8WGCrM1sPSIzQG70d1fuirRg5H7oeVRTPzpYN/cSXqRULk31Z9RneXwgZZZgPB\n" + + "Q1hE3oJmP4LJEfRhL4P7TL2Xp+1Kvius53my2zKnVXoBNlAUHSdidXsd+xVaOEkE\n" + + "cNyhLg4cZmlyuz5Ew/NHPAD70Cd9qXQraOf3dqZ77yhG0y/FCwXxnnfW1FnTLe14\n" + + "3RWuNAFhbuNuYrXn0Zq+SFz3UnNNKMoNejwDcvkxxZ92KQXJcB7zCRnEehjBz/At\n" + + "iNgsVfiOVRxzzp12iV+ljtM8A3KJHnnBQypPIeq4yKsXxtumVhryAc5k2neRZkvc\n" + + "Wo1x3T/EY0SSlFFSYsiyDgbaj0SguiVNTrJbLQd62a1S4ZCYB5k2hlzm22eIKHIa\n" + + "lb+sYaTGbSkxVH2xMvjxgO4dx9YvTlH6rsTIktmhvYxnF27Y6Bfhp+x8I3RoYPRC\n" + + "ImMgllybYE9AOHLI5uogvoe133OfHAmHVm36qx+S24r8YTMdZ6iJKLCd0Hav6aS9\n" + + "b4ptBiKQQQR2mtxaQNyBVEjfbpt2/ATnzRg5D/TAJATvhoeByWRYNP21iATnWU5c\n" + + "H3uK3dNDLnZAbaEf2XfqEG2fcw/Bn9mbXUodEay+EQl0Z11kWKOBSwMyGwSxdMhw\n" + + "S+9tTnfFZ73B/fyD41p3Ft02cUJcD2yW/j3+5JLOqZJJlTEhtAFvixkhcfR7VJTl\n" + + "arZfECPXOOMbiBxQmFA4+AZfP+9bMFPz9/guZTkIWsjKO4JI6ge6ayl6Eel2Qsbo\n" + + "MzsYA6m9h4a0VQPmHf1Itg5kiEpecG4rEqzJC2ov4mTiD4kVlPhUj6Je+VU3mEgT\n" + + "geMf/8JkD6+IParbR7iaEQF2wPgrR/VcBX/5Y8AI4mW8eiTCybtPt9z4X6w326Uy\n" + + "UkaqeswhQmd2sODDqxxrdjVmYQEWqVKIRRBLsR5fvDiqyiVFbPEO\n" + + "=qFHk\n" + + "-----END PGP MESSAGE-----\n"; + + private static final String PKESK3_SEIP_OPS3_OPS23_LIT_SIG23_SIG4 = "-----BEGIN PGP MESSAGE-----\n" + + "\n" + + "wcDMA3wvqk35PDeyAQv+JdNtgn26/Q4yXJ9egCmJ1/3SaeSB+y93jz+fmabMvc97\n" + + "KbI1HN9RoG23UxDoQED+jGcbbrw+7ho73U3uRC4NtE4D4SZZMQZZXTadI0MEu4eN\n" + + "yGgyFmZLy2Fek5N33m4PShvKqbeDnubmxv/lPlpz//KuxbVNPL+vtiVxZuI6vusB\n" + + "q3T0Br0kH7OqCizCIKzl85d9hYAKXDaCaGw/0VXmFs0HgjBSB5RRHKt3GpVt83dI\n" + + "b7vyyrzo6D0bwr+9nYC9Q+rku0lPNJutdSBRv9Xoc8eB7ud+0x9Ybj61C91gjvJp\n" + + "NkeuqcCYOZw+iFckDR8+GUM4T5PO3dwxEVQUUeO99EKuCH2S/PqgrH8JFiRcCvED\n" + + "SsngpAL3IfkoKCuAO0gpK4ebDKQ8R46ho/ER1UApT9A8lNbzLYIwCQ8fcK53AICe\n" + + "XWDTGb1uqqkt0vFPxsKA0R6Wyk6gA8j8ta6zpgTpwGOyRbMKo8QW08mw+2UROCPj\n" + + "/cdtV1Z6Iv0GrbKwrwB20sOlAYDKbSloZBhkwugYMqYWQfzPcM8S+sCX/+Kv/O79\n" + + "oWucvbSAiUv0JlD6zqdNcileqJKCiiAVaCUZhLpZcgVWpLqfJuFFnHBOo98ARIHe\n" + + "D5CzXn2sx+0ZlFW0fJk8Z2ZWXK9rKAleqsGB4dIA3WoC4UAFFjBqXG/4pa/H0He2\n" + + "G8R3Q+wFqEaXYgm2Znq/+UxPGjAJLH7EUrwfBvK17eByT+bLqyZpKHhuZJXy/rw1\n" + + "n+pCC1fedDWdxKj7+1Xw8cVlAWYCp2314DQjfI8BzFw0JWq8MU7hwGDQkJgfI3qH\n" + + "RlBhIgE2iJAOQi4YaSgC9QaAxL4Uw3uo9+kwUGt87j+M4d7ALQ1XXLtCim6P36jP\n" + + "kjOoAvfgwZ1NZEbzo/YS9/NK9KVXrhfgmkWSPjLaqJeur2Av9IkDuQmFF+EVxjRo\n" + + "eQzZHk8RHOj03jjTZH/QHNiiUDfr2cMDj8Hi+r5pCvS/T67gymyE6VHQJSYS9dXP\n" + + "0EMoe4jaG+aOdbAi3OPtKETSvtsKLflMR4k/RxRXN6lsV238wVna+w6nYZxqMqd4\n" + + "fNnL5YUHZOEt2qxVguOCEZDANHoR0RFVXM6yBF36Ivwjhg5a1aujyHv150KwDDsc\n" + + "YMI4O2pTcmhDX1aiV2X35EyLWJbSovbNj/IveMKa0q/xOXe4V8INX9Xv4sxm0mqY\n" + + "RR8CY1E3cYG2g6Uhc4WkirSvXoN/IRq1MrYmcCHQrEDDBL5h/8/TIn/TAOZqBrZ/\n" + + "gF1gfFW4ZgEcPZUeUmErHxLvdVqC+WK7/5qE86PXGo9yD9/Xxv5U7i8BbxgknUlD\n" + + "SyRmBfzkRJudTHvH9wnk9KA5hPHqXk6ZkrBo4ugaiwUa/EejvkiHW7KRWijH57nL\n" + + "JDzP13FU16gPuhPFTXP2zLvLeMSpVmkv2B9Mzvnrg+836B+hY0elAy5U/3D1/wxG\n" + + "IjfDTsAcUQ8yULCRj6iZrx1SZc0HJDe4mjvqVqg0VOSpxGHfRNcte+zh6p8Fqqly\n" + + "2O72vb49uEr54ZeL33j/ggCXWvMgdK7EtIirmzcwFsmamy89QSl1VzZzh5338n7f\n" + + "9SOd67xL8BVSh8lee8ByuBiZryLbIuy1d8stndbbxLi+W7+Y/W08g3QyE+NwcpKd\n" + + "/zTVViCZolgs4Ol73WEe6A131u+AMlJWXYD5tai+RmQOFugvCVX+QhezK1v3YrMH\n" + + "KlIfFsh4Cq+JIo2jMMoVjLBK662kU24w8eaEagdIjBgd1XlEBgKUR/f754BOfoKi\n" + + "JX2ySeHdQCCn/yc753X1TH3FNEThmJPHJG0ESkpIxqoTKdL3Ut+8BFlhWYwxCc8r\n" + + "R8m9ixq0cQBXrNVaJsFVKqI9H4SJMc8ySGe8HYwJV2hhK9HbuhAfrKiJoUmoQHvD\n" + + "jL9Y6H3ejK5YmqQ/zXoiepRfAklN3q+ByqhRMjZfDMuk0fcMaPy9RFoo1FqIPyqw\n" + + "alekNaR/K4albyRcMoYxBhn3QFHf7VuaPuaxhg1ri3YfrWykv3RA\n" + + "=jGpv\n" + + "-----END PGP MESSAGE-----\n"; + + private final PGPSecretKeyRing key; + private final PGPPublicKeyRing cert; + + public UnsupportedPacketVersionsTest() throws IOException { + key = PGPainless.readKeyRing().secretKeyRing(KEY); + cert = PGPainless.extractCertificate(key); + } + + @Test + public void pkesk3_pkesk23_seip() throws PGPException, IOException { + decryptAndCompare(PKESK3_PKESK23_SEIP, "Encrypted using SEIP + MDC."); + } + + @Test + public void pkesk23_pkesk_seip() throws PGPException, IOException { + decryptAndCompare(PKESK23_PKESK3_SEIP, "Encrypted using SEIP + MDC."); + } + + @Test + public void pkesk3_skesk23_seip() throws PGPException, IOException { + decryptAndCompare(PKESK3_SKESK23_SEIP, "Encrypted using SEIP + MDC."); + } + + @Test + public void skesk23_pkesk3_seip() throws PGPException, IOException { + decryptAndCompare(SKESK23_PKESK3_SEIP, "Encrypted using SEIP + MDC."); + } + + @Test + @Disabled("Enable once https://github.com/bcgit/bc-java/pull/1268 is available") + public void pkesk3_skesk4Ws2k23_seip() throws PGPException, IOException { + decryptAndCompare(PKESK3_SKESK4wS2K23_SEIP, "Encrypted using SEIP + MDC."); + } + + @Test + @Disabled("Enable once https://github.com/bcgit/bc-java/pull/1268 is available") + public void skesk4Ws2k23_pkesk3_seip() throws PGPException, IOException { + decryptAndCompare(SKESK4wS2K23_PKESK3_SEIP, "Encrypted using SEIP + MDC."); + } + + @Test + public void pkesk3_seip_ops3_lit_sig4() throws PGPException, IOException { + decryptAndCompare(PKESK3_SEIP_OPS3_LIT_SIG4, "Encrypted, signed message."); + } + + @Test + public void pkesk3_seip_ops23_lit_sig23() throws PGPException, IOException { + decryptAndCompare(PKESK3_SEIP_OPS23_LIT_SIG23, "Encrypted, signed message."); + } + + @Test + public void pkesk3_seip_ops23_ops3_lit_sig4_sig23() throws PGPException, IOException { + decryptAndCompare(PKESK3_SEIP_OPS23_OPS3_LIT_SIG4_SIG23, "Encrypted, signed message."); + } + + @Test + public void pkesk3_seip_ops3_ops23_lit_sig23_sig4() throws PGPException, IOException { + decryptAndCompare(PKESK3_SEIP_OPS3_OPS23_LIT_SIG23_SIG4, "Encrypted, signed message."); + } + + public void decryptAndCompare(String msg, String plain) throws IOException, PGPException { + // noinspection CharsetObjectCanBeUsed + ByteArrayInputStream inputStream = new ByteArrayInputStream(msg.getBytes(Charset.forName("UTF8"))); + DecryptionStream decryptionStream = PGPainless.decryptAndOrVerify() + .onInputStream(inputStream) + .withOptions(ConsumerOptions.get() + .addDecryptionKey(key) + .addVerificationCert(cert)); + + ByteArrayOutputStream out = new ByteArrayOutputStream(); + Streams.pipeAll(decryptionStream, out); + decryptionStream.close(); + + assertEquals(plain, out.toString()); + } +} From a0ba6828c982bfce4192162e45f6c853a976790a Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 26 Oct 2022 18:22:50 +0200 Subject: [PATCH 068/763] Remove superfluous states --- .../syntax_check/PDA.java | 26 +++---------------- .../syntax_check/StackAlphabet.java | 4 --- 2 files changed, 3 insertions(+), 27 deletions(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/PDA.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/PDA.java index 0d6ba28c..c77b50cc 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/PDA.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/PDA.java @@ -67,7 +67,7 @@ public class PDA { case Signature: if (stackItem == ops) { - return CorrespondingSignature; + return LiteralMessage; } else { throw new MalformedOpenPgpMessageException(this, input, stackItem); } @@ -96,7 +96,7 @@ public class PDA { switch (input) { case Signature: if (stackItem == ops) { - return CorrespondingSignature; + return CompressedMessage; } else { throw new MalformedOpenPgpMessageException(this, input, stackItem); } @@ -125,7 +125,7 @@ public class PDA { switch (input) { case Signature: if (stackItem == ops) { - return CorrespondingSignature; + return EncryptedMessage; } else { throw new MalformedOpenPgpMessageException(this, input, stackItem); } @@ -147,26 +147,6 @@ public class PDA { } }, - CorrespondingSignature { - @Override - State transition(InputAlphabet input, PDA automaton) throws MalformedOpenPgpMessageException { - StackAlphabet stackItem = automaton.popStack(); - if (input == InputAlphabet.EndOfSequence) { - if (stackItem == terminus && automaton.stack.isEmpty()) { - return Valid; - } else { - // premature end of stream - throw new MalformedOpenPgpMessageException(this, input, stackItem); - } - } else if (input == InputAlphabet.Signature) { - if (stackItem == ops) { - return CorrespondingSignature; - } - } - throw new MalformedOpenPgpMessageException(this, input, stackItem); - } - }, - Valid { @Override State transition(InputAlphabet input, PDA automaton) throws MalformedOpenPgpMessageException { diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/StackAlphabet.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/StackAlphabet.java index 6030fbc8..a8a2a213 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/StackAlphabet.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/StackAlphabet.java @@ -13,10 +13,6 @@ public enum StackAlphabet { * OnePassSignature (in case of BC this represents a OnePassSignatureList). */ ops, - /** - * ESK. Not used, as BC combines encrypted data with their encrypted session keys. - */ - esk, /** * Special symbol representing the end of the message. */ From a2a5c9223eaff33c9ec0f2d6ea7336c2e25d657f Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 26 Oct 2022 18:24:19 +0200 Subject: [PATCH 069/763] Remove debugging fields --- .../pgpainless/decryption_verification/syntax_check/PDA.java | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/PDA.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/PDA.java index c77b50cc..40021734 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/PDA.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/PDA.java @@ -16,7 +16,6 @@ import static org.pgpainless.decryption_verification.syntax_check.StackAlphabet. public class PDA { - private static int ID = 0; private static final Logger LOGGER = LoggerFactory.getLogger(PDA.class); /** @@ -171,13 +170,11 @@ public class PDA { private final Stack stack = new Stack<>(); private State state; - private int id; public PDA() { state = State.OpenPgpMessage; stack.push(terminus); stack.push(msg); - this.id = ID++; } public void next(InputAlphabet input) throws MalformedOpenPgpMessageException { @@ -240,6 +237,6 @@ public class PDA { @Override public String toString() { - return "PDA " + id + ": State: " + state + " Stack: " + stack; + return "State: " + state + " Stack: " + stack; } } From 798e68e87f4c76239564a988a887dc6beaa92c4c Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 26 Oct 2022 18:39:20 +0200 Subject: [PATCH 070/763] Improve syntax error reporting --- .../decryption_verification/syntax_check/PDA.java | 11 +++++++++-- .../exception/MalformedOpenPgpMessageException.java | 6 +++++- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/PDA.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/PDA.java index 40021734..550cb855 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/PDA.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/PDA.java @@ -8,6 +8,9 @@ import org.pgpainless.exception.MalformedOpenPgpMessageException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; import java.util.Stack; import static org.pgpainless.decryption_verification.syntax_check.StackAlphabet.msg; @@ -169,6 +172,7 @@ public class PDA { } private final Stack stack = new Stack<>(); + private final List inputs = new ArrayList<>(); // keep track of inputs for debugging / error reporting private State state; public PDA() { @@ -180,9 +184,12 @@ public class PDA { public void next(InputAlphabet input) throws MalformedOpenPgpMessageException { try { state = state.transition(input, this); + inputs.add(input); } catch (MalformedOpenPgpMessageException e) { - LOGGER.debug("Unexpected Packet or Token '" + input + "' encountered. Message is malformed.", e); - throw e; + MalformedOpenPgpMessageException wrapped = new MalformedOpenPgpMessageException("Malformed message: After reading stream " + Arrays.toString(inputs.toArray()) + + ", token '" + input + "' is unexpected and illegal.", e); + LOGGER.debug("Invalid input '" + input + "'", wrapped); + throw wrapped; } } diff --git a/pgpainless-core/src/main/java/org/pgpainless/exception/MalformedOpenPgpMessageException.java b/pgpainless-core/src/main/java/org/pgpainless/exception/MalformedOpenPgpMessageException.java index cbe78c05..aa158746 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/exception/MalformedOpenPgpMessageException.java +++ b/pgpainless-core/src/main/java/org/pgpainless/exception/MalformedOpenPgpMessageException.java @@ -21,6 +21,10 @@ public class MalformedOpenPgpMessageException extends RuntimeException { } public MalformedOpenPgpMessageException(PDA.State state, InputAlphabet input, StackAlphabet stackItem) { - this("Invalid input: There is no legal transition from state '" + state + "' for input '" + input + "' when '" + stackItem + "' is on top of the stack."); + this("There is no legal transition from state '" + state + "' for input '" + input + "' when '" + stackItem + "' is on top of the stack."); + } + + public MalformedOpenPgpMessageException(String s, MalformedOpenPgpMessageException e) { + super(s, e); } } From b3d61b0494a9c63d6037436fe600fa2769f1999c Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 27 Oct 2022 11:56:10 +0200 Subject: [PATCH 071/763] Separate out syntax logic --- .../syntax_check/OpenPgpMessageSyntax.java | 119 +++++++++++++ .../syntax_check/PDA.java | 165 ++---------------- .../syntax_check/State.java | 16 ++ .../syntax_check/Syntax.java | 13 ++ .../syntax_check/Transition.java | 28 +++ .../MalformedOpenPgpMessageException.java | 4 +- 6 files changed, 189 insertions(+), 156 deletions(-) create mode 100644 pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/OpenPgpMessageSyntax.java create mode 100644 pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/State.java create mode 100644 pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/Syntax.java create mode 100644 pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/Transition.java diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/OpenPgpMessageSyntax.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/OpenPgpMessageSyntax.java new file mode 100644 index 00000000..ab45a896 --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/OpenPgpMessageSyntax.java @@ -0,0 +1,119 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.decryption_verification.syntax_check; + +import org.pgpainless.exception.MalformedOpenPgpMessageException; + +public class OpenPgpMessageSyntax implements Syntax { + + @Override + public Transition transition(State from, InputAlphabet input, StackAlphabet stackItem) + throws MalformedOpenPgpMessageException { + switch (from) { + case OpenPgpMessage: + return fromOpenPgpMessage(input, stackItem); + case LiteralMessage: + return fromLiteralMessage(input, stackItem); + case CompressedMessage: + return fromCompressedMessage(input, stackItem); + case EncryptedMessage: + return fromEncryptedMessage(input, stackItem); + case Valid: + return fromValid(input, stackItem); + } + + throw new MalformedOpenPgpMessageException(from, input, stackItem); + } + + Transition fromOpenPgpMessage(InputAlphabet input, StackAlphabet stackItem) + throws MalformedOpenPgpMessageException { + if (stackItem != StackAlphabet.msg) { + throw new MalformedOpenPgpMessageException(State.OpenPgpMessage, input, stackItem); + } + + switch (input) { + case LiteralData: + return new Transition(State.LiteralMessage); + + case Signature: + return new Transition(State.OpenPgpMessage, StackAlphabet.msg); + + case OnePassSignature: + return new Transition(State.OpenPgpMessage, StackAlphabet.ops, StackAlphabet.msg); + + case CompressedData: + return new Transition(State.CompressedMessage); + + case EncryptedData: + return new Transition(State.EncryptedMessage); + + case EndOfSequence: + default: + throw new MalformedOpenPgpMessageException(State.OpenPgpMessage, input, stackItem); + } + } + + Transition fromLiteralMessage(InputAlphabet input, StackAlphabet stackItem) + throws MalformedOpenPgpMessageException { + switch (input) { + case Signature: + if (stackItem == StackAlphabet.ops) { + return new Transition(State.LiteralMessage); + } + break; + + case EndOfSequence: + if (stackItem == StackAlphabet.terminus) { + return new Transition(State.Valid); + } + break; + } + + throw new MalformedOpenPgpMessageException(State.LiteralMessage, input, stackItem); + } + + Transition fromCompressedMessage(InputAlphabet input, StackAlphabet stackItem) + throws MalformedOpenPgpMessageException { + switch (input) { + case Signature: + if (stackItem == StackAlphabet.ops) { + return new Transition(State.CompressedMessage); + } + break; + + case EndOfSequence: + if (stackItem == StackAlphabet.terminus) { + return new Transition(State.Valid); + } + break; + } + + throw new MalformedOpenPgpMessageException(State.CompressedMessage, input, stackItem); + } + + Transition fromEncryptedMessage(InputAlphabet input, StackAlphabet stackItem) + throws MalformedOpenPgpMessageException { + switch (input) { + case Signature: + if (stackItem == StackAlphabet.ops) { + return new Transition(State.EncryptedMessage); + } + break; + + case EndOfSequence: + if (stackItem == StackAlphabet.terminus) { + return new Transition(State.Valid); + } + break; + } + + throw new MalformedOpenPgpMessageException(State.EncryptedMessage, input, stackItem); + } + + Transition fromValid(InputAlphabet input, StackAlphabet stackItem) + throws MalformedOpenPgpMessageException { + throw new MalformedOpenPgpMessageException(State.Valid, input, stackItem); + } +} diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/PDA.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/PDA.java index 550cb855..94c1739e 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/PDA.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/PDA.java @@ -14,177 +14,31 @@ import java.util.List; import java.util.Stack; import static org.pgpainless.decryption_verification.syntax_check.StackAlphabet.msg; -import static org.pgpainless.decryption_verification.syntax_check.StackAlphabet.ops; import static org.pgpainless.decryption_verification.syntax_check.StackAlphabet.terminus; public class PDA { private static final Logger LOGGER = LoggerFactory.getLogger(PDA.class); - /** - * Set of states of the automaton. - * Each state defines its valid transitions in their {@link State#transition(InputAlphabet, PDA)} method. - */ - public enum State { - - OpenPgpMessage { - @Override - State transition(InputAlphabet input, PDA automaton) throws MalformedOpenPgpMessageException { - StackAlphabet stackItem = automaton.popStack(); - if (stackItem != msg) { - throw new MalformedOpenPgpMessageException(this, input, stackItem); - } - switch (input) { - - case LiteralData: - return LiteralMessage; - - case Signature: - automaton.pushStack(msg); - return OpenPgpMessage; - - case OnePassSignature: - automaton.pushStack(ops); - automaton.pushStack(msg); - return OpenPgpMessage; - - case CompressedData: - return CompressedMessage; - - case EncryptedData: - return EncryptedMessage; - - case EndOfSequence: - default: - throw new MalformedOpenPgpMessageException(this, input, stackItem); - } - } - }, - - LiteralMessage { - @Override - State transition(InputAlphabet input, PDA automaton) throws MalformedOpenPgpMessageException { - StackAlphabet stackItem = automaton.popStack(); - switch (input) { - - case Signature: - if (stackItem == ops) { - return LiteralMessage; - } else { - throw new MalformedOpenPgpMessageException(this, input, stackItem); - } - - case EndOfSequence: - if (stackItem == terminus && automaton.stack.isEmpty()) { - return Valid; - } else { - throw new MalformedOpenPgpMessageException(this, input, stackItem); - } - - case LiteralData: - case OnePassSignature: - case CompressedData: - case EncryptedData: - default: - throw new MalformedOpenPgpMessageException(this, input, stackItem); - } - } - }, - - CompressedMessage { - @Override - State transition(InputAlphabet input, PDA automaton) throws MalformedOpenPgpMessageException { - StackAlphabet stackItem = automaton.popStack(); - switch (input) { - case Signature: - if (stackItem == ops) { - return CompressedMessage; - } else { - throw new MalformedOpenPgpMessageException(this, input, stackItem); - } - - case EndOfSequence: - if (stackItem == terminus && automaton.stack.isEmpty()) { - return Valid; - } else { - throw new MalformedOpenPgpMessageException(this, input, stackItem); - } - - case LiteralData: - case OnePassSignature: - case CompressedData: - case EncryptedData: - default: - throw new MalformedOpenPgpMessageException(this, input, stackItem); - } - } - }, - - EncryptedMessage { - @Override - State transition(InputAlphabet input, PDA automaton) throws MalformedOpenPgpMessageException { - StackAlphabet stackItem = automaton.popStack(); - switch (input) { - case Signature: - if (stackItem == ops) { - return EncryptedMessage; - } else { - throw new MalformedOpenPgpMessageException(this, input, stackItem); - } - - case EndOfSequence: - if (stackItem == terminus && automaton.stack.isEmpty()) { - return Valid; - } else { - throw new MalformedOpenPgpMessageException(this, input, stackItem); - } - - case LiteralData: - case OnePassSignature: - case CompressedData: - case EncryptedData: - default: - throw new MalformedOpenPgpMessageException(this, input, stackItem); - } - } - }, - - Valid { - @Override - State transition(InputAlphabet input, PDA automaton) throws MalformedOpenPgpMessageException { - throw new MalformedOpenPgpMessageException(this, input, null); - } - }, - ; - - /** - * Pop the automatons stack and transition to another state. - * If no valid transition from the current state is available given the popped stack item and input symbol, - * a {@link MalformedOpenPgpMessageException} is thrown. - * Otherwise, the stack is manipulated according to the valid transition and the new state is returned. - * - * @param input input symbol - * @param automaton automaton - * @return new state of the automaton - * @throws MalformedOpenPgpMessageException in case of an illegal input symbol - */ - abstract State transition(InputAlphabet input, PDA automaton) throws MalformedOpenPgpMessageException; - } - private final Stack stack = new Stack<>(); private final List inputs = new ArrayList<>(); // keep track of inputs for debugging / error reporting private State state; + private Syntax syntax = new OpenPgpMessageSyntax(); public PDA() { state = State.OpenPgpMessage; - stack.push(terminus); - stack.push(msg); + pushStack(terminus); + pushStack(msg); } public void next(InputAlphabet input) throws MalformedOpenPgpMessageException { try { - state = state.transition(input, this); + Transition transition = syntax.transition(state, input, popStack()); inputs.add(input); + state = transition.getNewState(); + for (StackAlphabet item : transition.getPushedItems()) { + pushStack(item); + } } catch (MalformedOpenPgpMessageException e) { MalformedOpenPgpMessageException wrapped = new MalformedOpenPgpMessageException("Malformed message: After reading stream " + Arrays.toString(inputs.toArray()) + ", token '" + input + "' is unexpected and illegal.", e); @@ -230,6 +84,9 @@ public class PDA { * @return stack item */ private StackAlphabet popStack() { + if (stack.isEmpty()) { + return null; + } return stack.pop(); } diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/State.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/State.java new file mode 100644 index 00000000..9dee9af1 --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/State.java @@ -0,0 +1,16 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.decryption_verification.syntax_check; + +/** + * Set of states of the automaton. + */ +public enum State { + OpenPgpMessage, + LiteralMessage, + CompressedMessage, + EncryptedMessage, + Valid +} diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/Syntax.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/Syntax.java new file mode 100644 index 00000000..47813a9e --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/Syntax.java @@ -0,0 +1,13 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.decryption_verification.syntax_check; + +import org.pgpainless.exception.MalformedOpenPgpMessageException; + +public interface Syntax { + + Transition transition(State from, InputAlphabet inputAlphabet, StackAlphabet stackItem) + throws MalformedOpenPgpMessageException; +} diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/Transition.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/Transition.java new file mode 100644 index 00000000..bbc28e58 --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/Transition.java @@ -0,0 +1,28 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.decryption_verification.syntax_check; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +public class Transition { + + private final List pushedItems = new ArrayList<>(); + private final State newState; + + public Transition(State newState, StackAlphabet... pushedItems) { + this.newState = newState; + this.pushedItems.addAll(Arrays.asList(pushedItems)); + } + + public State getNewState() { + return newState; + } + + public List getPushedItems() { + return new ArrayList<>(pushedItems); + } +} diff --git a/pgpainless-core/src/main/java/org/pgpainless/exception/MalformedOpenPgpMessageException.java b/pgpainless-core/src/main/java/org/pgpainless/exception/MalformedOpenPgpMessageException.java index aa158746..db8d0df6 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/exception/MalformedOpenPgpMessageException.java +++ b/pgpainless-core/src/main/java/org/pgpainless/exception/MalformedOpenPgpMessageException.java @@ -5,8 +5,8 @@ package org.pgpainless.exception; import org.pgpainless.decryption_verification.syntax_check.InputAlphabet; -import org.pgpainless.decryption_verification.syntax_check.PDA; import org.pgpainless.decryption_verification.syntax_check.StackAlphabet; +import org.pgpainless.decryption_verification.syntax_check.State; /** * Exception that gets thrown if the OpenPGP message is malformed. @@ -20,7 +20,7 @@ public class MalformedOpenPgpMessageException extends RuntimeException { super(message); } - public MalformedOpenPgpMessageException(PDA.State state, InputAlphabet input, StackAlphabet stackItem) { + public MalformedOpenPgpMessageException(State state, InputAlphabet input, StackAlphabet stackItem) { this("There is no legal transition from state '" + state + "' for input '" + input + "' when '" + stackItem + "' is on top of the stack."); } From 8ca0cfd3ae9dff01ded689c434dd5d6cb9f6d6e7 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 27 Oct 2022 12:07:22 +0200 Subject: [PATCH 072/763] Rename *Alphabet to *Symbol and add javadoc --- .../OpenPgpMessageInputStream.java | 18 ++-- .../{InputAlphabet.java => InputSymbol.java} | 2 +- .../syntax_check/OpenPgpMessageSyntax.java | 41 ++++--- .../syntax_check/PDA.java | 18 ++-- .../{StackAlphabet.java => StackSymbol.java} | 2 +- .../syntax_check/Syntax.java | 19 +++- .../syntax_check/Transition.java | 6 +- .../MalformedOpenPgpMessageException.java | 6 +- .../syntax_check/PDATest.java | 102 +++++++++--------- 9 files changed, 121 insertions(+), 93 deletions(-) rename pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/{InputAlphabet.java => InputSymbol.java} (98%) rename pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/{StackAlphabet.java => StackSymbol.java} (93%) diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java index 75b78294..a0f17210 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java @@ -45,9 +45,9 @@ import org.pgpainless.algorithm.EncryptionPurpose; import org.pgpainless.algorithm.OpenPgpPacket; import org.pgpainless.algorithm.StreamEncoding; import org.pgpainless.algorithm.SymmetricKeyAlgorithm; -import org.pgpainless.decryption_verification.syntax_check.InputAlphabet; +import org.pgpainless.decryption_verification.syntax_check.InputSymbol; import org.pgpainless.decryption_verification.syntax_check.PDA; -import org.pgpainless.decryption_verification.syntax_check.StackAlphabet; +import org.pgpainless.decryption_verification.syntax_check.StackSymbol; import org.pgpainless.decryption_verification.cleartext_signatures.ClearsignedMessageUtil; import org.pgpainless.decryption_verification.cleartext_signatures.MultiPassStrategy; import org.pgpainless.exception.MalformedOpenPgpMessageException; @@ -334,7 +334,7 @@ public class OpenPgpMessageInputStream extends DecryptionStream { private void processLiteralData() throws IOException { LOGGER.debug("Literal Data Packet at depth " + metadata.depth + " encountered"); - syntaxVerifier.next(InputAlphabet.LiteralData); + syntaxVerifier.next(InputSymbol.LiteralData); PGPLiteralData literalData = packetInputStream.readLiteralData(); this.metadata.setChild(new MessageMetadata.LiteralData( literalData.getFileName(), @@ -344,7 +344,7 @@ public class OpenPgpMessageInputStream extends DecryptionStream { } private void processCompressedData() throws IOException, PGPException { - syntaxVerifier.next(InputAlphabet.CompressedData); + syntaxVerifier.next(InputSymbol.CompressedData); signatures.enterNesting(); PGPCompressedData compressedData = packetInputStream.readCompressedData(); MessageMetadata.CompressedData compressionLayer = new MessageMetadata.CompressedData( @@ -356,7 +356,7 @@ public class OpenPgpMessageInputStream extends DecryptionStream { } private void processOnePassSignature() throws PGPException, IOException { - syntaxVerifier.next(InputAlphabet.OnePassSignature); + syntaxVerifier.next(InputSymbol.OnePassSignature); PGPOnePassSignature onePassSignature = packetInputStream.readOnePassSignature(); LOGGER.debug("One-Pass-Signature Packet by key " + KeyIdUtil.formatKeyId(onePassSignature.getKeyID()) + " at depth " + metadata.depth + " encountered"); @@ -365,8 +365,8 @@ public class OpenPgpMessageInputStream extends DecryptionStream { private void processSignature() throws PGPException, IOException { // true if Signature corresponds to OnePassSignature - boolean isSigForOPS = syntaxVerifier.peekStack() == StackAlphabet.ops; - syntaxVerifier.next(InputAlphabet.Signature); + boolean isSigForOPS = syntaxVerifier.peekStack() == StackSymbol.ops; + syntaxVerifier.next(InputSymbol.Signature); PGPSignature signature; try { signature = packetInputStream.readSignature(); @@ -391,7 +391,7 @@ public class OpenPgpMessageInputStream extends DecryptionStream { private boolean processEncryptedData() throws IOException, PGPException { LOGGER.debug("Symmetrically Encrypted Data Packet at depth " + metadata.depth + " encountered"); - syntaxVerifier.next(InputAlphabet.EncryptedData); + syntaxVerifier.next(InputSymbol.EncryptedData); PGPEncryptedDataList encDataList = packetInputStream.readEncryptedDataList(); // TODO: Replace with !encDataList.isIntegrityProtected() @@ -766,7 +766,7 @@ public class OpenPgpMessageInputStream extends DecryptionStream { } if (packetInputStream != null) { - syntaxVerifier.next(InputAlphabet.EndOfSequence); + syntaxVerifier.next(InputSymbol.EndOfSequence); syntaxVerifier.assertValid(); packetInputStream.close(); } diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/InputAlphabet.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/InputSymbol.java similarity index 98% rename from pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/InputAlphabet.java rename to pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/InputSymbol.java index f73ede34..854c3305 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/InputAlphabet.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/InputSymbol.java @@ -10,7 +10,7 @@ import org.bouncycastle.openpgp.PGPLiteralData; import org.bouncycastle.openpgp.PGPOnePassSignatureList; import org.bouncycastle.openpgp.PGPSignatureList; -public enum InputAlphabet { +public enum InputSymbol { /** * A {@link PGPLiteralData} packet. */ diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/OpenPgpMessageSyntax.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/OpenPgpMessageSyntax.java index ab45a896..4c811e9f 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/OpenPgpMessageSyntax.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/OpenPgpMessageSyntax.java @@ -6,10 +6,20 @@ package org.pgpainless.decryption_verification.syntax_check; import org.pgpainless.exception.MalformedOpenPgpMessageException; +/** + * This class describes the syntax for OpenPGP messages as specified by rfc4880. + * + * @see + * rfc4880 - §11.3. OpenPGP Messages + * @see + * Blog post about theoretic background and translation of grammar to PDA syntax + * @see + * Blog post about practically implementing the PDA for packet syntax validation + */ public class OpenPgpMessageSyntax implements Syntax { @Override - public Transition transition(State from, InputAlphabet input, StackAlphabet stackItem) + public Transition transition(State from, InputSymbol input, StackSymbol stackItem) throws MalformedOpenPgpMessageException { switch (from) { case OpenPgpMessage: @@ -27,9 +37,9 @@ public class OpenPgpMessageSyntax implements Syntax { throw new MalformedOpenPgpMessageException(from, input, stackItem); } - Transition fromOpenPgpMessage(InputAlphabet input, StackAlphabet stackItem) + Transition fromOpenPgpMessage(InputSymbol input, StackSymbol stackItem) throws MalformedOpenPgpMessageException { - if (stackItem != StackAlphabet.msg) { + if (stackItem != StackSymbol.msg) { throw new MalformedOpenPgpMessageException(State.OpenPgpMessage, input, stackItem); } @@ -38,10 +48,10 @@ public class OpenPgpMessageSyntax implements Syntax { return new Transition(State.LiteralMessage); case Signature: - return new Transition(State.OpenPgpMessage, StackAlphabet.msg); + return new Transition(State.OpenPgpMessage, StackSymbol.msg); case OnePassSignature: - return new Transition(State.OpenPgpMessage, StackAlphabet.ops, StackAlphabet.msg); + return new Transition(State.OpenPgpMessage, StackSymbol.ops, StackSymbol.msg); case CompressedData: return new Transition(State.CompressedMessage); @@ -55,17 +65,17 @@ public class OpenPgpMessageSyntax implements Syntax { } } - Transition fromLiteralMessage(InputAlphabet input, StackAlphabet stackItem) + Transition fromLiteralMessage(InputSymbol input, StackSymbol stackItem) throws MalformedOpenPgpMessageException { switch (input) { case Signature: - if (stackItem == StackAlphabet.ops) { + if (stackItem == StackSymbol.ops) { return new Transition(State.LiteralMessage); } break; case EndOfSequence: - if (stackItem == StackAlphabet.terminus) { + if (stackItem == StackSymbol.terminus) { return new Transition(State.Valid); } break; @@ -74,17 +84,17 @@ public class OpenPgpMessageSyntax implements Syntax { throw new MalformedOpenPgpMessageException(State.LiteralMessage, input, stackItem); } - Transition fromCompressedMessage(InputAlphabet input, StackAlphabet stackItem) + Transition fromCompressedMessage(InputSymbol input, StackSymbol stackItem) throws MalformedOpenPgpMessageException { switch (input) { case Signature: - if (stackItem == StackAlphabet.ops) { + if (stackItem == StackSymbol.ops) { return new Transition(State.CompressedMessage); } break; case EndOfSequence: - if (stackItem == StackAlphabet.terminus) { + if (stackItem == StackSymbol.terminus) { return new Transition(State.Valid); } break; @@ -93,17 +103,17 @@ public class OpenPgpMessageSyntax implements Syntax { throw new MalformedOpenPgpMessageException(State.CompressedMessage, input, stackItem); } - Transition fromEncryptedMessage(InputAlphabet input, StackAlphabet stackItem) + Transition fromEncryptedMessage(InputSymbol input, StackSymbol stackItem) throws MalformedOpenPgpMessageException { switch (input) { case Signature: - if (stackItem == StackAlphabet.ops) { + if (stackItem == StackSymbol.ops) { return new Transition(State.EncryptedMessage); } break; case EndOfSequence: - if (stackItem == StackAlphabet.terminus) { + if (stackItem == StackSymbol.terminus) { return new Transition(State.Valid); } break; @@ -112,8 +122,9 @@ public class OpenPgpMessageSyntax implements Syntax { throw new MalformedOpenPgpMessageException(State.EncryptedMessage, input, stackItem); } - Transition fromValid(InputAlphabet input, StackAlphabet stackItem) + Transition fromValid(InputSymbol input, StackSymbol stackItem) throws MalformedOpenPgpMessageException { + // There is no applicable transition rule out of Valid throw new MalformedOpenPgpMessageException(State.Valid, input, stackItem); } } diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/PDA.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/PDA.java index 94c1739e..68a5e2c4 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/PDA.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/PDA.java @@ -13,15 +13,15 @@ import java.util.Arrays; import java.util.List; import java.util.Stack; -import static org.pgpainless.decryption_verification.syntax_check.StackAlphabet.msg; -import static org.pgpainless.decryption_verification.syntax_check.StackAlphabet.terminus; +import static org.pgpainless.decryption_verification.syntax_check.StackSymbol.msg; +import static org.pgpainless.decryption_verification.syntax_check.StackSymbol.terminus; public class PDA { private static final Logger LOGGER = LoggerFactory.getLogger(PDA.class); - private final Stack stack = new Stack<>(); - private final List inputs = new ArrayList<>(); // keep track of inputs for debugging / error reporting + private final Stack stack = new Stack<>(); + private final List inputs = new ArrayList<>(); // keep track of inputs for debugging / error reporting private State state; private Syntax syntax = new OpenPgpMessageSyntax(); @@ -31,12 +31,12 @@ public class PDA { pushStack(msg); } - public void next(InputAlphabet input) throws MalformedOpenPgpMessageException { + public void next(InputSymbol input) throws MalformedOpenPgpMessageException { try { Transition transition = syntax.transition(state, input, popStack()); inputs.add(input); state = transition.getNewState(); - for (StackAlphabet item : transition.getPushedItems()) { + for (StackSymbol item : transition.getPushedItems()) { pushStack(item); } } catch (MalformedOpenPgpMessageException e) { @@ -56,7 +56,7 @@ public class PDA { return state; } - public StackAlphabet peekStack() { + public StackSymbol peekStack() { if (stack.isEmpty()) { return null; } @@ -83,7 +83,7 @@ public class PDA { * * @return stack item */ - private StackAlphabet popStack() { + private StackSymbol popStack() { if (stack.isEmpty()) { return null; } @@ -95,7 +95,7 @@ public class PDA { * * @param item item */ - private void pushStack(StackAlphabet item) { + private void pushStack(StackSymbol item) { stack.push(item); } diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/StackAlphabet.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/StackSymbol.java similarity index 93% rename from pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/StackAlphabet.java rename to pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/StackSymbol.java index a8a2a213..120458e5 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/StackAlphabet.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/StackSymbol.java @@ -4,7 +4,7 @@ package org.pgpainless.decryption_verification.syntax_check; -public enum StackAlphabet { +public enum StackSymbol { /** * OpenPGP Message. */ diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/Syntax.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/Syntax.java index 47813a9e..63d63fed 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/Syntax.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/Syntax.java @@ -6,8 +6,25 @@ package org.pgpainless.decryption_verification.syntax_check; import org.pgpainless.exception.MalformedOpenPgpMessageException; +/** + * This interface can be used to define a custom syntax for the {@link PDA}. + */ public interface Syntax { - Transition transition(State from, InputAlphabet inputAlphabet, StackAlphabet stackItem) + /** + * Describe a transition rule from {@link State}
from
for {@link InputSymbol}
input
+ * with {@link StackSymbol}
stackItem
from the top of the {@link PDA PDAs} stack. + * The resulting {@link Transition} contains the new {@link State}, as well as a list of + * {@link StackSymbol StackSymbols} that get pushed onto the stack by the transition rule. + * If there is no applicable rule, a {@link MalformedOpenPgpMessageException} is thrown, since in this case + * the {@link InputSymbol} must be considered illegal. + * + * @param from current state of the PDA + * @param input input symbol + * @param stackItem item that got popped from the top of the stack + * @return applicable transition rule containing the new state and pushed stack symbols + * @throws MalformedOpenPgpMessageException if there is no applicable transition rule (the input symbol is illegal) + */ + Transition transition(State from, InputSymbol input, StackSymbol stackItem) throws MalformedOpenPgpMessageException; } diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/Transition.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/Transition.java index bbc28e58..a0e58cf0 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/Transition.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/Transition.java @@ -10,10 +10,10 @@ import java.util.List; public class Transition { - private final List pushedItems = new ArrayList<>(); + private final List pushedItems = new ArrayList<>(); private final State newState; - public Transition(State newState, StackAlphabet... pushedItems) { + public Transition(State newState, StackSymbol... pushedItems) { this.newState = newState; this.pushedItems.addAll(Arrays.asList(pushedItems)); } @@ -22,7 +22,7 @@ public class Transition { return newState; } - public List getPushedItems() { + public List getPushedItems() { return new ArrayList<>(pushedItems); } } diff --git a/pgpainless-core/src/main/java/org/pgpainless/exception/MalformedOpenPgpMessageException.java b/pgpainless-core/src/main/java/org/pgpainless/exception/MalformedOpenPgpMessageException.java index db8d0df6..f98a4048 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/exception/MalformedOpenPgpMessageException.java +++ b/pgpainless-core/src/main/java/org/pgpainless/exception/MalformedOpenPgpMessageException.java @@ -4,8 +4,8 @@ package org.pgpainless.exception; -import org.pgpainless.decryption_verification.syntax_check.InputAlphabet; -import org.pgpainless.decryption_verification.syntax_check.StackAlphabet; +import org.pgpainless.decryption_verification.syntax_check.InputSymbol; +import org.pgpainless.decryption_verification.syntax_check.StackSymbol; import org.pgpainless.decryption_verification.syntax_check.State; /** @@ -20,7 +20,7 @@ public class MalformedOpenPgpMessageException extends RuntimeException { super(message); } - public MalformedOpenPgpMessageException(State state, InputAlphabet input, StackAlphabet stackItem) { + public MalformedOpenPgpMessageException(State state, InputSymbol input, StackSymbol stackItem) { this("There is no legal transition from state '" + state + "' for input '" + input + "' when '" + stackItem + "' is on top of the stack."); } diff --git a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/syntax_check/PDATest.java b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/syntax_check/PDATest.java index 9250acfa..2b66eee0 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/syntax_check/PDATest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/syntax_check/PDATest.java @@ -21,8 +21,8 @@ public class PDATest { @Test public void testSimpleLiteralMessageIsValid() throws MalformedOpenPgpMessageException { PDA check = new PDA(); - check.next(InputAlphabet.LiteralData); - check.next(InputAlphabet.EndOfSequence); + check.next(InputSymbol.LiteralData); + check.next(InputSymbol.EndOfSequence); assertTrue(check.isValid()); } @@ -35,10 +35,10 @@ public class PDATest { @Test public void testSimpleOpsSignedMesssageIsValid() throws MalformedOpenPgpMessageException { PDA check = new PDA(); - check.next(InputAlphabet.OnePassSignature); - check.next(InputAlphabet.LiteralData); - check.next(InputAlphabet.Signature); - check.next(InputAlphabet.EndOfSequence); + check.next(InputSymbol.OnePassSignature); + check.next(InputSymbol.LiteralData); + check.next(InputSymbol.Signature); + check.next(InputSymbol.EndOfSequence); assertTrue(check.isValid()); } @@ -52,9 +52,9 @@ public class PDATest { @Test public void testSimplePrependSignedMessageIsValid() throws MalformedOpenPgpMessageException { PDA check = new PDA(); - check.next(InputAlphabet.Signature); - check.next(InputAlphabet.LiteralData); - check.next(InputAlphabet.EndOfSequence); + check.next(InputSymbol.Signature); + check.next(InputSymbol.LiteralData); + check.next(InputSymbol.EndOfSequence); assertTrue(check.isValid()); } @@ -68,11 +68,11 @@ public class PDATest { @Test public void testOPSSignedCompressedMessageIsValid() throws MalformedOpenPgpMessageException { PDA check = new PDA(); - check.next(InputAlphabet.OnePassSignature); - check.next(InputAlphabet.CompressedData); + check.next(InputSymbol.OnePassSignature); + check.next(InputSymbol.CompressedData); // Here would be a nested PDA for the LiteralData packet - check.next(InputAlphabet.Signature); - check.next(InputAlphabet.EndOfSequence); + check.next(InputSymbol.Signature); + check.next(InputSymbol.EndOfSequence); assertTrue(check.isValid()); } @@ -80,105 +80,105 @@ public class PDATest { @Test public void testOPSSignedEncryptedMessageIsValid() { PDA check = new PDA(); - check.next(InputAlphabet.OnePassSignature); - check.next(InputAlphabet.EncryptedData); - check.next(InputAlphabet.Signature); - check.next(InputAlphabet.EndOfSequence); + check.next(InputSymbol.OnePassSignature); + check.next(InputSymbol.EncryptedData); + check.next(InputSymbol.Signature); + check.next(InputSymbol.EndOfSequence); assertTrue(check.isValid()); } @Test public void anyInputAfterEOSIsNotValid() { PDA check = new PDA(); - check.next(InputAlphabet.LiteralData); - check.next(InputAlphabet.EndOfSequence); + check.next(InputSymbol.LiteralData); + check.next(InputSymbol.EndOfSequence); assertThrows(MalformedOpenPgpMessageException.class, - () -> check.next(InputAlphabet.Signature)); + () -> check.next(InputSymbol.Signature)); } @Test public void testEncryptedMessageWithAppendedStandalongSigIsNotValid() { PDA check = new PDA(); - check.next(InputAlphabet.EncryptedData); + check.next(InputSymbol.EncryptedData); assertThrows(MalformedOpenPgpMessageException.class, - () -> check.next(InputAlphabet.Signature)); + () -> check.next(InputSymbol.Signature)); } @Test public void testOPSSignedEncryptedMessageWithMissingSigIsNotValid() { PDA check = new PDA(); - check.next(InputAlphabet.OnePassSignature); - check.next(InputAlphabet.EncryptedData); + check.next(InputSymbol.OnePassSignature); + check.next(InputSymbol.EncryptedData); assertThrows(MalformedOpenPgpMessageException.class, - () -> check.next(InputAlphabet.EndOfSequence)); + () -> check.next(InputSymbol.EndOfSequence)); } @Test public void testTwoLiteralDataIsNotValid() { PDA check = new PDA(); - check.next(InputAlphabet.LiteralData); + check.next(InputSymbol.LiteralData); assertThrows(MalformedOpenPgpMessageException.class, - () -> check.next(InputAlphabet.LiteralData)); + () -> check.next(InputSymbol.LiteralData)); } @Test public void testTrailingSigIsNotValid() { PDA check = new PDA(); - check.next(InputAlphabet.LiteralData); + check.next(InputSymbol.LiteralData); assertThrows(MalformedOpenPgpMessageException.class, - () -> check.next(InputAlphabet.Signature)); + () -> check.next(InputSymbol.Signature)); } @Test public void testOPSAloneIsNotValid() { PDA check = new PDA(); - check.next(InputAlphabet.OnePassSignature); + check.next(InputSymbol.OnePassSignature); assertThrows(MalformedOpenPgpMessageException.class, - () -> check.next(InputAlphabet.EndOfSequence)); + () -> check.next(InputSymbol.EndOfSequence)); } @Test public void testOPSLitWithMissingSigIsNotValid() { PDA check = new PDA(); - check.next(InputAlphabet.OnePassSignature); - check.next(InputAlphabet.LiteralData); + check.next(InputSymbol.OnePassSignature); + check.next(InputSymbol.LiteralData); assertThrows(MalformedOpenPgpMessageException.class, - () -> check.next(InputAlphabet.EndOfSequence)); + () -> check.next(InputSymbol.EndOfSequence)); } @Test public void testCompressedMessageWithStandalongAppendedSigIsNotValid() { PDA check = new PDA(); - check.next(InputAlphabet.CompressedData); + check.next(InputSymbol.CompressedData); assertThrows(MalformedOpenPgpMessageException.class, - () -> check.next(InputAlphabet.Signature)); + () -> check.next(InputSymbol.Signature)); } @Test public void testOPSCompressedDataWithMissingSigIsNotValid() { PDA check = new PDA(); - check.next(InputAlphabet.OnePassSignature); - check.next(InputAlphabet.CompressedData); + check.next(InputSymbol.OnePassSignature); + check.next(InputSymbol.CompressedData); assertThrows(MalformedOpenPgpMessageException.class, - () -> check.next(InputAlphabet.EndOfSequence)); + () -> check.next(InputSymbol.EndOfSequence)); } @Test public void testCompressedMessageFollowedByTrailingLiteralDataIsNotValid() { PDA check = new PDA(); - check.next(InputAlphabet.CompressedData); + check.next(InputSymbol.CompressedData); assertThrows(MalformedOpenPgpMessageException.class, - () -> check.next(InputAlphabet.LiteralData)); + () -> check.next(InputSymbol.LiteralData)); } @Test public void testOPSWithPrependedSigIsValid() { PDA check = new PDA(); - check.next(InputAlphabet.Signature); - check.next(InputAlphabet.OnePassSignature); - check.next(InputAlphabet.LiteralData); - check.next(InputAlphabet.Signature); - check.next(InputAlphabet.EndOfSequence); + check.next(InputSymbol.Signature); + check.next(InputSymbol.OnePassSignature); + check.next(InputSymbol.LiteralData); + check.next(InputSymbol.Signature); + check.next(InputSymbol.EndOfSequence); assertTrue(check.isValid()); } @@ -186,11 +186,11 @@ public class PDATest { @Test public void testPrependedSigInsideOPSSignedMessageIsValid() { PDA check = new PDA(); - check.next(InputAlphabet.OnePassSignature); - check.next(InputAlphabet.Signature); - check.next(InputAlphabet.LiteralData); - check.next(InputAlphabet.Signature); - check.next(InputAlphabet.EndOfSequence); + check.next(InputSymbol.OnePassSignature); + check.next(InputSymbol.Signature); + check.next(InputSymbol.LiteralData); + check.next(InputSymbol.Signature); + check.next(InputSymbol.EndOfSequence); assertTrue(check.isValid()); } From ec793c66ffab343ab32cb659e32c279be9279a90 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 27 Oct 2022 12:42:30 +0200 Subject: [PATCH 073/763] More cleanup and better error reporting --- .../syntax_check/OpenPgpMessageSyntax.java | 15 ++++++++----- .../syntax_check/PDA.java | 19 +++++++++++----- .../syntax_check/Syntax.java | 5 ++++- .../syntax_check/Transition.java | 22 ++++++++++++++++++- .../syntax_check/PDATest.java | 2 +- 5 files changed, 48 insertions(+), 15 deletions(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/OpenPgpMessageSyntax.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/OpenPgpMessageSyntax.java index 4c811e9f..c6de8765 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/OpenPgpMessageSyntax.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/OpenPgpMessageSyntax.java @@ -6,6 +6,9 @@ package org.pgpainless.decryption_verification.syntax_check; import org.pgpainless.exception.MalformedOpenPgpMessageException; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + /** * This class describes the syntax for OpenPGP messages as specified by rfc4880. * @@ -19,7 +22,7 @@ import org.pgpainless.exception.MalformedOpenPgpMessageException; public class OpenPgpMessageSyntax implements Syntax { @Override - public Transition transition(State from, InputSymbol input, StackSymbol stackItem) + public @Nonnull Transition transition(@Nonnull State from, @Nonnull InputSymbol input, @Nullable StackSymbol stackItem) throws MalformedOpenPgpMessageException { switch (from) { case OpenPgpMessage: @@ -37,7 +40,7 @@ public class OpenPgpMessageSyntax implements Syntax { throw new MalformedOpenPgpMessageException(from, input, stackItem); } - Transition fromOpenPgpMessage(InputSymbol input, StackSymbol stackItem) + Transition fromOpenPgpMessage(@Nonnull InputSymbol input, @Nullable StackSymbol stackItem) throws MalformedOpenPgpMessageException { if (stackItem != StackSymbol.msg) { throw new MalformedOpenPgpMessageException(State.OpenPgpMessage, input, stackItem); @@ -65,7 +68,7 @@ public class OpenPgpMessageSyntax implements Syntax { } } - Transition fromLiteralMessage(InputSymbol input, StackSymbol stackItem) + Transition fromLiteralMessage(@Nonnull InputSymbol input, @Nullable StackSymbol stackItem) throws MalformedOpenPgpMessageException { switch (input) { case Signature: @@ -84,7 +87,7 @@ public class OpenPgpMessageSyntax implements Syntax { throw new MalformedOpenPgpMessageException(State.LiteralMessage, input, stackItem); } - Transition fromCompressedMessage(InputSymbol input, StackSymbol stackItem) + Transition fromCompressedMessage(@Nonnull InputSymbol input, @Nullable StackSymbol stackItem) throws MalformedOpenPgpMessageException { switch (input) { case Signature: @@ -103,7 +106,7 @@ public class OpenPgpMessageSyntax implements Syntax { throw new MalformedOpenPgpMessageException(State.CompressedMessage, input, stackItem); } - Transition fromEncryptedMessage(InputSymbol input, StackSymbol stackItem) + Transition fromEncryptedMessage(@Nonnull InputSymbol input, @Nullable StackSymbol stackItem) throws MalformedOpenPgpMessageException { switch (input) { case Signature: @@ -122,7 +125,7 @@ public class OpenPgpMessageSyntax implements Syntax { throw new MalformedOpenPgpMessageException(State.EncryptedMessage, input, stackItem); } - Transition fromValid(InputSymbol input, StackSymbol stackItem) + Transition fromValid(@Nonnull InputSymbol input, @Nullable StackSymbol stackItem) throws MalformedOpenPgpMessageException { // There is no applicable transition rule out of Valid throw new MalformedOpenPgpMessageException(State.Valid, input, stackItem); diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/PDA.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/PDA.java index 68a5e2c4..ed9175fd 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/PDA.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/PDA.java @@ -20,10 +20,13 @@ public class PDA { private static final Logger LOGGER = LoggerFactory.getLogger(PDA.class); + // right now we implement what rfc4880 specifies. + // TODO: Consider implementing what we proposed here: + // https://mailarchive.ietf.org/arch/msg/openpgp/uepOF6XpSegMO4c59tt9e5H1i4g/ + private final Syntax syntax = new OpenPgpMessageSyntax(); private final Stack stack = new Stack<>(); - private final List inputs = new ArrayList<>(); // keep track of inputs for debugging / error reporting + private final List inputs = new ArrayList<>(); // Track inputs for debugging / error reporting private State state; - private Syntax syntax = new OpenPgpMessageSyntax(); public PDA() { state = State.OpenPgpMessage; @@ -32,16 +35,20 @@ public class PDA { } public void next(InputSymbol input) throws MalformedOpenPgpMessageException { + StackSymbol stackSymbol = popStack(); try { - Transition transition = syntax.transition(state, input, popStack()); - inputs.add(input); + Transition transition = syntax.transition(state, input, stackSymbol); state = transition.getNewState(); for (StackSymbol item : transition.getPushedItems()) { pushStack(item); } + inputs.add(input); } catch (MalformedOpenPgpMessageException e) { - MalformedOpenPgpMessageException wrapped = new MalformedOpenPgpMessageException("Malformed message: After reading stream " + Arrays.toString(inputs.toArray()) + - ", token '" + input + "' is unexpected and illegal.", e); + MalformedOpenPgpMessageException wrapped = new MalformedOpenPgpMessageException( + "Malformed message: After reading stream " + Arrays.toString(inputs.toArray()) + + ", token '" + input + "' is not allowed." + + "\nNo transition from state '" + state + "' with stack " + Arrays.toString(stack.toArray()) + + (stackSymbol != null ? "||'" + stackSymbol + "'." : "."), e); LOGGER.debug("Invalid input '" + input + "'", wrapped); throw wrapped; } diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/Syntax.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/Syntax.java index 63d63fed..2f3d0a57 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/Syntax.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/Syntax.java @@ -6,6 +6,9 @@ package org.pgpainless.decryption_verification.syntax_check; import org.pgpainless.exception.MalformedOpenPgpMessageException; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + /** * This interface can be used to define a custom syntax for the {@link PDA}. */ @@ -25,6 +28,6 @@ public interface Syntax { * @return applicable transition rule containing the new state and pushed stack symbols * @throws MalformedOpenPgpMessageException if there is no applicable transition rule (the input symbol is illegal) */ - Transition transition(State from, InputSymbol input, StackSymbol stackItem) + @Nonnull Transition transition(@Nonnull State from, @Nonnull InputSymbol input, @Nullable StackSymbol stackItem) throws MalformedOpenPgpMessageException; } diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/Transition.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/Transition.java index a0e58cf0..ab0db5ef 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/Transition.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/Transition.java @@ -4,24 +4,44 @@ package org.pgpainless.decryption_verification.syntax_check; +import javax.annotation.Nonnull; import java.util.ArrayList; import java.util.Arrays; import java.util.List; +/** + * Result of applying a transition rule. + * Transition rules can be described by implementing the {@link Syntax} interface. + */ public class Transition { private final List pushedItems = new ArrayList<>(); private final State newState; - public Transition(State newState, StackSymbol... pushedItems) { + public Transition(@Nonnull State newState, @Nonnull StackSymbol... pushedItems) { this.newState = newState; this.pushedItems.addAll(Arrays.asList(pushedItems)); } + /** + * Return the new {@link State} that is reached by applying the transition. + * + * @return new state + */ + @Nonnull public State getNewState() { return newState; } + /** + * Return a list of {@link StackSymbol StackSymbols} that are pushed onto the stack + * by applying the transition. + * The list contains items in the order in which they are pushed onto the stack. + * The list may be empty. + * + * @return list of items to be pushed onto the stack + */ + @Nonnull public List getPushedItems() { return new ArrayList<>(pushedItems); } diff --git a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/syntax_check/PDATest.java b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/syntax_check/PDATest.java index 2b66eee0..d0486a3e 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/syntax_check/PDATest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/syntax_check/PDATest.java @@ -97,7 +97,7 @@ public class PDATest { } @Test - public void testEncryptedMessageWithAppendedStandalongSigIsNotValid() { + public void testEncryptedMessageWithAppendedStandaloneSigIsNotValid() { PDA check = new PDA(); check.next(InputSymbol.EncryptedData); assertThrows(MalformedOpenPgpMessageException.class, From 161ce577115f20c797f79d83d36655c3d0834c6e Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 27 Oct 2022 12:47:42 +0200 Subject: [PATCH 074/763] Clean up old unused code --- .../DecryptionBuilderInterface.java | 2 +- .../DecryptionStreamImpl.java | 65 ------ .../SignatureInputStream.java | 215 ------------------ .../exception/FinalIOException.java | 17 -- .../MissingLiteralDataException.java | 17 -- 5 files changed, 1 insertion(+), 315 deletions(-) delete mode 100644 pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionStreamImpl.java delete mode 100644 pgpainless-core/src/main/java/org/pgpainless/decryption_verification/SignatureInputStream.java delete mode 100644 pgpainless-core/src/main/java/org/pgpainless/exception/FinalIOException.java delete mode 100644 pgpainless-core/src/main/java/org/pgpainless/exception/MissingLiteralDataException.java 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 b35911de..07db42f0 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 @@ -13,7 +13,7 @@ import org.bouncycastle.openpgp.PGPException; public interface DecryptionBuilderInterface { /** - * Create a {@link DecryptionStreamImpl} on an {@link InputStream} which contains the encrypted and/or signed data. + * Create a {@link DecryptionStream} on an {@link InputStream} which contains the encrypted and/or signed data. * * @param inputStream encrypted and/or signed data. * @return api handle diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionStreamImpl.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionStreamImpl.java deleted file mode 100644 index 27ace697..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionStreamImpl.java +++ /dev/null @@ -1,65 +0,0 @@ -// SPDX-FileCopyrightText: 2018 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.decryption_verification; - -import java.io.IOException; -import java.io.InputStream; -import javax.annotation.Nonnull; - -import org.bouncycastle.util.io.Streams; - -/** - * Decryption Stream that handles updating and verification of detached signatures, - * as well as verification of integrity-protected input streams once the stream gets closed. - */ -public class DecryptionStreamImpl extends DecryptionStream { - - private final InputStream inputStream; - private final IntegrityProtectedInputStream integrityProtectedInputStream; - private final InputStream armorStream; - - /** - * Create an input stream that handles decryption and - if necessary - integrity protection verification. - * - * @param wrapped underlying input stream - * @param resultBuilder builder for decryption metadata like algorithms, recipients etc. - * @param integrityProtectedInputStream in case of data encrypted using SEIP packet close this stream to check integrity - * @param armorStream armor stream to verify CRC checksums - */ - DecryptionStreamImpl(@Nonnull InputStream wrapped, - @Nonnull OpenPgpMetadata.Builder resultBuilder, - IntegrityProtectedInputStream integrityProtectedInputStream, - InputStream armorStream) { - super(resultBuilder); - this.inputStream = wrapped; - this.integrityProtectedInputStream = integrityProtectedInputStream; - this.armorStream = armorStream; - } - - @Override - public void close() throws IOException { - if (armorStream != null) { - Streams.drain(armorStream); - } - inputStream.close(); - if (integrityProtectedInputStream != null) { - integrityProtectedInputStream.close(); - } - super.close(); - } - - @Override - public int read() throws IOException { - int r = inputStream.read(); - return r; - } - - @Override - public int read(@Nonnull byte[] bytes, int offset, int length) throws IOException { - int read = inputStream.read(bytes, offset, length); - return read; - } - -} diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/SignatureInputStream.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/SignatureInputStream.java deleted file mode 100644 index 70a2f4ef..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/SignatureInputStream.java +++ /dev/null @@ -1,215 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.decryption_verification; - -import static org.pgpainless.signature.consumer.SignatureValidator.signatureWasCreatedInBounds; - -import java.io.FilterInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.util.List; -import java.util.Map; -import javax.annotation.Nonnull; -import javax.annotation.Nullable; - -import org.bouncycastle.openpgp.PGPObjectFactory; -import org.bouncycastle.openpgp.PGPPublicKeyRing; -import org.bouncycastle.openpgp.PGPSignature; -import org.bouncycastle.openpgp.PGPSignatureList; -import org.pgpainless.PGPainless; -import org.pgpainless.exception.SignatureValidationException; -import org.pgpainless.policy.Policy; -import org.pgpainless.signature.consumer.CertificateValidator; -import org.pgpainless.signature.consumer.SignatureCheck; -import org.pgpainless.signature.consumer.OnePassSignatureCheck; -import org.pgpainless.signature.SignatureUtils; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -public abstract class SignatureInputStream extends FilterInputStream { - - protected SignatureInputStream(InputStream inputStream) { - super(inputStream); - } - - public static class VerifySignatures extends SignatureInputStream { - - private static final Logger LOGGER = LoggerFactory.getLogger(VerifySignatures.class); - - private final PGPObjectFactory objectFactory; - private final List opSignatures; - private final Map opSignaturesWithMissingCert; - private final List detachedSignatures; - private final ConsumerOptions options; - private final OpenPgpMetadata.Builder resultBuilder; - - public VerifySignatures( - InputStream literalDataStream, - @Nullable PGPObjectFactory objectFactory, - List opSignatures, - Map onePassSignaturesWithMissingCert, - List detachedSignatures, - ConsumerOptions options, - OpenPgpMetadata.Builder resultBuilder) { - super(literalDataStream); - this.objectFactory = objectFactory; - this.opSignatures = opSignatures; - this.opSignaturesWithMissingCert = onePassSignaturesWithMissingCert; - this.detachedSignatures = detachedSignatures; - this.options = options; - this.resultBuilder = resultBuilder; - } - - @Override - public int read() throws IOException { - final int data = super.read(); - final boolean endOfStream = data == -1; - if (endOfStream) { - finalizeSignatures(); - } else { - byte b = (byte) data; - updateOnePassSignatures(b); - updateDetachedSignatures(b); - } - return data; - } - - @Override - public int read(@Nonnull byte[] b, int off, int len) throws IOException { - int read = super.read(b, off, len); - - final boolean endOfStream = read == -1; - if (endOfStream) { - finalizeSignatures(); - } else { - updateOnePassSignatures(b, off, read); - updateDetachedSignatures(b, off, read); - } - return read; - } - - private void finalizeSignatures() { - parseAndCombineSignatures(); - verifyOnePassSignatures(); - verifyDetachedSignatures(); - } - - public void parseAndCombineSignatures() { - if (objectFactory == null) { - return; - } - // Parse signatures from message - PGPSignatureList signatures; - try { - signatures = parseSignatures(objectFactory); - } catch (IOException e) { - return; - } - List signatureList = SignatureUtils.toList(signatures); - // Set signatures as comparison sigs in OPS checks - for (int i = 0; i < opSignatures.size(); i++) { - int reversedIndex = opSignatures.size() - i - 1; - opSignatures.get(i).setSignature(signatureList.get(reversedIndex)); - } - - for (PGPSignature signature : signatureList) { - if (opSignaturesWithMissingCert.containsKey(signature.getKeyID())) { - OnePassSignatureCheck check = opSignaturesWithMissingCert.remove(signature.getKeyID()); - check.setSignature(signature); - - resultBuilder.addInvalidInbandSignature(new SignatureVerification(signature, null), - new SignatureValidationException( - "Missing verification certificate " + Long.toHexString(signature.getKeyID()))); - } - } - } - - private PGPSignatureList parseSignatures(PGPObjectFactory objectFactory) throws IOException { - PGPSignatureList signatureList = null; - Object pgpObject = objectFactory.nextObject(); - while (pgpObject != null && signatureList == null) { - if (pgpObject instanceof PGPSignatureList) { - signatureList = (PGPSignatureList) pgpObject; - } else { - pgpObject = objectFactory.nextObject(); - } - } - - if (signatureList == null || signatureList.isEmpty()) { - throw new IOException("Verification failed - No Signatures found"); - } - - return signatureList; - } - - - private synchronized void verifyOnePassSignatures() { - Policy policy = PGPainless.getPolicy(); - for (OnePassSignatureCheck opSignature : opSignatures) { - if (opSignature.getSignature() == null) { - LOGGER.warn("Found OnePassSignature without respective signature packet -> skip"); - continue; - } - - try { - signatureWasCreatedInBounds(options.getVerifyNotBefore(), - options.getVerifyNotAfter()).verify(opSignature.getSignature()); - CertificateValidator.validateCertificateAndVerifyOnePassSignature(opSignature, policy); - resultBuilder.addVerifiedInbandSignature( - new SignatureVerification(opSignature.getSignature(), opSignature.getSigningKey())); - } catch (SignatureValidationException e) { - LOGGER.warn("One-pass-signature verification failed for signature made by key {}: {}", - opSignature.getSigningKey(), e.getMessage(), e); - resultBuilder.addInvalidInbandSignature( - new SignatureVerification(opSignature.getSignature(), opSignature.getSigningKey()), e); - } - } - } - - private void verifyDetachedSignatures() { - Policy policy = PGPainless.getPolicy(); - for (SignatureCheck s : detachedSignatures) { - try { - signatureWasCreatedInBounds(options.getVerifyNotBefore(), - options.getVerifyNotAfter()).verify(s.getSignature()); - CertificateValidator.validateCertificateAndVerifyInitializedSignature(s.getSignature(), - (PGPPublicKeyRing) s.getSigningKeyRing(), policy); - resultBuilder.addVerifiedDetachedSignature(new SignatureVerification(s.getSignature(), - s.getSigningKeyIdentifier())); - } catch (SignatureValidationException e) { - LOGGER.warn("One-pass-signature verification failed for signature made by key {}: {}", - s.getSigningKeyIdentifier(), e.getMessage(), e); - resultBuilder.addInvalidDetachedSignature(new SignatureVerification(s.getSignature(), - s.getSigningKeyIdentifier()), e); - } - } - } - - private void updateOnePassSignatures(byte data) { - for (OnePassSignatureCheck opSignature : opSignatures) { - opSignature.getOnePassSignature().update(data); - } - } - - private void updateOnePassSignatures(byte[] bytes, int offset, int length) { - for (OnePassSignatureCheck opSignature : opSignatures) { - opSignature.getOnePassSignature().update(bytes, offset, length); - } - } - - private void updateDetachedSignatures(byte b) { - for (SignatureCheck detachedSignature : detachedSignatures) { - detachedSignature.getSignature().update(b); - } - } - - private void updateDetachedSignatures(byte[] b, int off, int read) { - for (SignatureCheck detachedSignature : detachedSignatures) { - detachedSignature.getSignature().update(b, off, read); - } - } - - } -} diff --git a/pgpainless-core/src/main/java/org/pgpainless/exception/FinalIOException.java b/pgpainless-core/src/main/java/org/pgpainless/exception/FinalIOException.java deleted file mode 100644 index 6b6f86de..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/exception/FinalIOException.java +++ /dev/null @@ -1,17 +0,0 @@ -// SPDX-FileCopyrightText: 2022 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.exception; - -import java.io.IOException; - -/** - * Wrapper for {@link IOException} indicating that we need to throw this exception up. - */ -public class FinalIOException extends IOException { - - public FinalIOException(IOException e) { - super(e); - } -} diff --git a/pgpainless-core/src/main/java/org/pgpainless/exception/MissingLiteralDataException.java b/pgpainless-core/src/main/java/org/pgpainless/exception/MissingLiteralDataException.java deleted file mode 100644 index e396b1df..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/exception/MissingLiteralDataException.java +++ /dev/null @@ -1,17 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.exception; - -import org.bouncycastle.openpgp.PGPException; - -/** - * Exception that gets thrown if a {@link org.bouncycastle.bcpg.LiteralDataPacket} is expected, but not found. - */ -public class MissingLiteralDataException extends PGPException { - - public MissingLiteralDataException(String message) { - super(message); - } -} From 8cb7d19487d01d4cf0746782068bff500713c2b4 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 27 Oct 2022 13:13:27 +0200 Subject: [PATCH 075/763] Allow injection of different syntax into PDA --- .../syntax_check/PDA.java | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/PDA.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/PDA.java index ed9175fd..65210ce0 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/PDA.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/PDA.java @@ -8,6 +8,7 @@ import org.pgpainless.exception.MalformedOpenPgpMessageException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import javax.annotation.Nonnull; import java.util.ArrayList; import java.util.Arrays; import java.util.List; @@ -23,15 +24,24 @@ public class PDA { // right now we implement what rfc4880 specifies. // TODO: Consider implementing what we proposed here: // https://mailarchive.ietf.org/arch/msg/openpgp/uepOF6XpSegMO4c59tt9e5H1i4g/ - private final Syntax syntax = new OpenPgpMessageSyntax(); + private final Syntax syntax; private final Stack stack = new Stack<>(); private final List inputs = new ArrayList<>(); // Track inputs for debugging / error reporting private State state; + /** + * + */ public PDA() { - state = State.OpenPgpMessage; - pushStack(terminus); - pushStack(msg); + this(new OpenPgpMessageSyntax(), State.OpenPgpMessage, terminus, msg); + } + + public PDA(@Nonnull Syntax syntax, @Nonnull State initialState, @Nonnull StackSymbol... initialStack) { + this.syntax = syntax; + this.state = initialState; + for (StackSymbol symbol : initialStack) { + pushStack(symbol); + } } public void next(InputSymbol input) throws MalformedOpenPgpMessageException { From 208612ab56d96c0785d9938b31265644023debdc Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 27 Oct 2022 13:53:54 +0200 Subject: [PATCH 076/763] Add (commented-out) read(buf, off, len) implementation for DelayedTeeInputStream --- .../TeeBCPGInputStream.java | 32 +++++++++++++++---- 1 file changed, 26 insertions(+), 6 deletions(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/TeeBCPGInputStream.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/TeeBCPGInputStream.java index bbcf593e..52ec9001 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/TeeBCPGInputStream.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/TeeBCPGInputStream.java @@ -26,20 +26,20 @@ import org.pgpainless.algorithm.OpenPgpPacket; * {@link BCPGInputStream#readPacket()} inconsistently calls a mix of {@link BCPGInputStream#read()} and * {@link InputStream#read()} of the underlying stream. This would cause the second length byte to get swallowed up. * - * Therefore, this class delegates the teeing to an {@link DelayedTeeInputStreamInputStream} which wraps the underlying + * Therefore, this class delegates the teeing to an {@link DelayedTeeInputStream} which wraps the underlying * stream. Since calling {@link BCPGInputStream#nextPacketTag()} reads up to and including the next packets tag, * we need to delay teeing out that byte to signature verifiers. * Hence, the reading methods of the {@link TeeBCPGInputStream} handle pushing this byte to the output stream using - * {@link DelayedTeeInputStreamInputStream#squeeze()}. + * {@link DelayedTeeInputStream#squeeze()}. */ public class TeeBCPGInputStream { - protected final DelayedTeeInputStreamInputStream delayedTee; + protected final DelayedTeeInputStream delayedTee; // InputStream of OpenPGP packets of the current layer protected final BCPGInputStream packetInputStream; public TeeBCPGInputStream(BCPGInputStream inputStream, OutputStream outputStream) { - this.delayedTee = new DelayedTeeInputStreamInputStream(inputStream, outputStream); + this.delayedTee = new DelayedTeeInputStream(inputStream, outputStream); this.packetInputStream = BCPGInputStream.wrap(delayedTee); } @@ -100,13 +100,13 @@ public class TeeBCPGInputStream { this.packetInputStream.close(); } - public static class DelayedTeeInputStreamInputStream extends InputStream { + public static class DelayedTeeInputStream extends InputStream { private int last = -1; private final InputStream inputStream; private final OutputStream outputStream; - public DelayedTeeInputStreamInputStream(InputStream inputStream, OutputStream outputStream) { + public DelayedTeeInputStream(InputStream inputStream, OutputStream outputStream) { this.inputStream = inputStream; this.outputStream = outputStream; } @@ -127,6 +127,26 @@ public class TeeBCPGInputStream { } } + // TODO: Uncomment, once BC-172.1 is available + // see https://github.com/bcgit/bc-java/issues/1257 + /* + @Override + public int read(byte[] b, int off, int len) throws IOException { + if (last != -1) { + outputStream.write(last); + } + + int r = inputStream.read(b, off, len); + if (r > 0) { + outputStream.write(b, off, r - 1); + last = b[off + r - 1]; + } else { + last = -1; + } + return r; + } + */ + /** * Squeeze the last byte out and update the output stream. * From 8fafb6aa562255c7961c7061520c2d5db21eae4b Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 27 Oct 2022 13:55:58 +0200 Subject: [PATCH 077/763] Add comments --- .../OpenPgpMessageInputStream.java | 14 ++++++++++++-- .../decryption_verification/syntax_check/PDA.java | 2 +- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java index a0f17210..1b11744c 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java @@ -116,6 +116,7 @@ public class OpenPgpMessageInputStream extends DecryptionStream { /** * Create an {@link OpenPgpMessageInputStream} suitable for decryption and verification of * OpenPGP messages and signatures. + * This factory method takes a custom {@link Policy} instead of using the global policy object. * * @param inputStream underlying input stream containing the OpenPGP message * @param options options for consuming the message @@ -161,7 +162,7 @@ public class OpenPgpMessageInputStream extends DecryptionStream { armorIn, options, metadata, policy); } } else { - throw new AssertionError("Huh?"); + throw new AssertionError("Cannot deduce type of data."); } } @@ -212,6 +213,7 @@ public class OpenPgpMessageInputStream extends DecryptionStream { switch (type) { + // Binary OpenPGP Message case standard: // tee out packet bytes for signature verification packetInputStream = new TeeBCPGInputStream(BCPGInputStream.wrap(inputStream), this.signatures); @@ -220,6 +222,7 @@ public class OpenPgpMessageInputStream extends DecryptionStream { consumePackets(); break; + // Cleartext Signature Framework (probably signed message) case cleartext_signed: resultBuilder.setCleartextSigned(); MultiPassStrategy multiPassStrategy = options.getMultiPassStrategy(); @@ -235,6 +238,7 @@ public class OpenPgpMessageInputStream extends DecryptionStream { nestedInputStream = new TeeInputStream(multiPassStrategy.getMessageInputStream(), this.signatures); break; + // Non-OpenPGP Data (e.g. detached signature verification) case non_openpgp: packetInputStream = null; nestedInputStream = new TeeInputStream(inputStream, this.signatures); @@ -265,7 +269,7 @@ public class OpenPgpMessageInputStream extends DecryptionStream { return; } - loop: // we break this when we go deeper. + loop: // we break this when we enter nested packets and later resume while ((nextPacket = packetInputStream.nextPacketTag()) != null) { signatures.nextPacket(nextPacket); switch (nextPacket) { @@ -296,6 +300,7 @@ public class OpenPgpMessageInputStream extends DecryptionStream { case SED: case SEIPD: if (processEncryptedData()) { + // Successfully decrypted, enter nested content break loop; } @@ -336,10 +341,12 @@ public class OpenPgpMessageInputStream extends DecryptionStream { LOGGER.debug("Literal Data Packet at depth " + metadata.depth + " encountered"); syntaxVerifier.next(InputSymbol.LiteralData); PGPLiteralData literalData = packetInputStream.readLiteralData(); + // Extract Metadata this.metadata.setChild(new MessageMetadata.LiteralData( literalData.getFileName(), literalData.getModificationTime(), StreamEncoding.requireFromCode(literalData.getFormat()))); + nestedInputStream = literalData.getDataStream(); } @@ -347,9 +354,11 @@ public class OpenPgpMessageInputStream extends DecryptionStream { syntaxVerifier.next(InputSymbol.CompressedData); signatures.enterNesting(); PGPCompressedData compressedData = packetInputStream.readCompressedData(); + // Extract Metadata MessageMetadata.CompressedData compressionLayer = new MessageMetadata.CompressedData( CompressionAlgorithm.fromId(compressedData.getAlgorithm()), metadata.depth + 1); + LOGGER.debug("Compressed Data Packet (" + compressionLayer.algorithm + ") at depth " + metadata.depth + " encountered"); InputStream decompressed = compressedData.getDataStream(); nestedInputStream = new OpenPgpMessageInputStream(decompressed, options, compressionLayer, policy); @@ -374,6 +383,7 @@ public class OpenPgpMessageInputStream extends DecryptionStream { LOGGER.debug("Unsupported Signature at depth " + metadata.depth + " encountered.", e); return; } + long keyId = SignatureUtils.determineIssuerKeyId(signature); if (isSigForOPS) { LOGGER.debug("Signature Packet corresponding to One-Pass-Signature by key " + diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/PDA.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/PDA.java index 65210ce0..07a5fdcb 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/PDA.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/PDA.java @@ -30,7 +30,7 @@ public class PDA { private State state; /** - * + * Default constructor which initializes the PDA to work with the {@link OpenPgpMessageSyntax}. */ public PDA() { this(new OpenPgpMessageSyntax(), State.OpenPgpMessage, terminus, msg); From 705e36080c6030056143e7adb0af917d9a075a3d Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Fri, 23 Sep 2022 14:51:06 +0200 Subject: [PATCH 078/763] Implement caching PublicKeyDataDecryptorFactory --- .../CachingPublicKeyDataDecryptorFactory.java | 75 +++++++++++++++++++ .../java/org/bouncycastle/package-info.java | 8 ++ 2 files changed, 83 insertions(+) create mode 100644 pgpainless-core/src/main/java/org/bouncycastle/CachingPublicKeyDataDecryptorFactory.java create mode 100644 pgpainless-core/src/main/java/org/bouncycastle/package-info.java diff --git a/pgpainless-core/src/main/java/org/bouncycastle/CachingPublicKeyDataDecryptorFactory.java b/pgpainless-core/src/main/java/org/bouncycastle/CachingPublicKeyDataDecryptorFactory.java new file mode 100644 index 00000000..1498b6f2 --- /dev/null +++ b/pgpainless-core/src/main/java/org/bouncycastle/CachingPublicKeyDataDecryptorFactory.java @@ -0,0 +1,75 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.bouncycastle; + +import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.operator.PGPDataDecryptor; +import org.bouncycastle.openpgp.operator.PublicKeyDataDecryptorFactory; +import org.bouncycastle.util.encoders.Base64; + +import java.util.HashMap; +import java.util.Map; + +/** + * Implementation of the {@link PublicKeyDataDecryptorFactory} which caches decrypted session keys. + * That way, if a message needs to be decrypted multiple times, expensive private key operations can be omitted. + * + * This implementation changes the behavior or {@link #recoverSessionData(int, byte[][])} to first return any + * cache hits. + * If no hit is found, the method call is delegated to the underlying {@link PublicKeyDataDecryptorFactory}. + * The result of that is then placed in the cache and returned. + * + * TODO: Do we also cache invalid session keys? + */ +public class CachingPublicKeyDataDecryptorFactory implements PublicKeyDataDecryptorFactory { + + private final Map cachedSessionKeys = new HashMap<>(); + private final PublicKeyDataDecryptorFactory factory; + + public CachingPublicKeyDataDecryptorFactory(PublicKeyDataDecryptorFactory factory) { + this.factory = factory; + } + + @Override + public byte[] recoverSessionData(int keyAlgorithm, byte[][] secKeyData) throws PGPException { + byte[] sessionKey = lookup(secKeyData); + if (sessionKey == null) { + sessionKey = factory.recoverSessionData(keyAlgorithm, secKeyData); + cache(secKeyData, sessionKey); + } + return sessionKey; + } + + private byte[] lookup(byte[][] secKeyData) { + byte[] sk = secKeyData[0]; + String key = Base64.toBase64String(sk); + byte[] sessionKey = cachedSessionKeys.get(key); + return copy(sessionKey); + } + + private void cache(byte[][] secKeyData, byte[] sessionKey) { + byte[] sk = secKeyData[0]; + String key = Base64.toBase64String(sk); + cachedSessionKeys.put(key, copy(sessionKey)); + } + + private static byte[] copy(byte[] bytes) { + if (bytes == null) { + return null; + } + byte[] copy = new byte[bytes.length]; + System.arraycopy(bytes, 0, copy, 0, copy.length); + return copy; + } + + public void clear() { + cachedSessionKeys.clear(); + } + + @Override + public PGPDataDecryptor createDataDecryptor(boolean withIntegrityPacket, int encAlgorithm, byte[] key) throws PGPException { + return null; + } +} diff --git a/pgpainless-core/src/main/java/org/bouncycastle/package-info.java b/pgpainless-core/src/main/java/org/bouncycastle/package-info.java new file mode 100644 index 00000000..565bb5f4 --- /dev/null +++ b/pgpainless-core/src/main/java/org/bouncycastle/package-info.java @@ -0,0 +1,8 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +/** + * Classes which could be upstreamed to BC at some point. + */ +package org.bouncycastle; From 8c0d096fc6d34566f937ae26fb49cae5dc38d534 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Fri, 28 Oct 2022 14:56:06 +0200 Subject: [PATCH 079/763] Fix CachingBcPublicKeyDataDecryptorFactory --- ...chingBcPublicKeyDataDecryptorFactory.java} | 44 ++++++--- ...ngBcPublicKeyDataDecryptorFactoryTest.java | 96 +++++++++++++++++++ 2 files changed, 126 insertions(+), 14 deletions(-) rename pgpainless-core/src/main/java/org/bouncycastle/{CachingPublicKeyDataDecryptorFactory.java => CachingBcPublicKeyDataDecryptorFactory.java} (52%) create mode 100644 pgpainless-core/src/test/java/bouncycastle/CachingBcPublicKeyDataDecryptorFactoryTest.java diff --git a/pgpainless-core/src/main/java/org/bouncycastle/CachingPublicKeyDataDecryptorFactory.java b/pgpainless-core/src/main/java/org/bouncycastle/CachingBcPublicKeyDataDecryptorFactory.java similarity index 52% rename from pgpainless-core/src/main/java/org/bouncycastle/CachingPublicKeyDataDecryptorFactory.java rename to pgpainless-core/src/main/java/org/bouncycastle/CachingBcPublicKeyDataDecryptorFactory.java index 1498b6f2..3c967224 100644 --- a/pgpainless-core/src/main/java/org/bouncycastle/CachingPublicKeyDataDecryptorFactory.java +++ b/pgpainless-core/src/main/java/org/bouncycastle/CachingBcPublicKeyDataDecryptorFactory.java @@ -5,9 +5,15 @@ package org.bouncycastle; import org.bouncycastle.openpgp.PGPException; -import org.bouncycastle.openpgp.operator.PGPDataDecryptor; +import org.bouncycastle.openpgp.PGPPrivateKey; import org.bouncycastle.openpgp.operator.PublicKeyDataDecryptorFactory; +import org.bouncycastle.openpgp.operator.bc.BcPublicKeyDataDecryptorFactory; import org.bouncycastle.util.encoders.Base64; +import org.bouncycastle.util.encoders.Hex; +import org.pgpainless.decryption_verification.CustomPublicKeyDataDecryptorFactory; +import org.pgpainless.key.SubkeyIdentifier; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import java.util.HashMap; import java.util.Map; @@ -20,36 +26,46 @@ import java.util.Map; * cache hits. * If no hit is found, the method call is delegated to the underlying {@link PublicKeyDataDecryptorFactory}. * The result of that is then placed in the cache and returned. - * - * TODO: Do we also cache invalid session keys? */ -public class CachingPublicKeyDataDecryptorFactory implements PublicKeyDataDecryptorFactory { +public class CachingBcPublicKeyDataDecryptorFactory + extends BcPublicKeyDataDecryptorFactory + implements CustomPublicKeyDataDecryptorFactory { + + private static final Logger LOGGER = LoggerFactory.getLogger(CachingBcPublicKeyDataDecryptorFactory.class); private final Map cachedSessionKeys = new HashMap<>(); - private final PublicKeyDataDecryptorFactory factory; + private final SubkeyIdentifier decryptionKey; - public CachingPublicKeyDataDecryptorFactory(PublicKeyDataDecryptorFactory factory) { - this.factory = factory; + public CachingBcPublicKeyDataDecryptorFactory(PGPPrivateKey privateKey, SubkeyIdentifier decryptionKey) { + super(privateKey); + this.decryptionKey = decryptionKey; } @Override public byte[] recoverSessionData(int keyAlgorithm, byte[][] secKeyData) throws PGPException { - byte[] sessionKey = lookup(secKeyData); + byte[] sessionKey = lookupSessionKeyData(secKeyData); if (sessionKey == null) { - sessionKey = factory.recoverSessionData(keyAlgorithm, secKeyData); - cache(secKeyData, sessionKey); + LOGGER.debug("Cache miss for encrypted session key " + Hex.toHexString(secKeyData[0])); + sessionKey = costlyRecoverSessionData(keyAlgorithm, secKeyData); + cacheSessionKeyData(secKeyData, sessionKey); + } else { + LOGGER.debug("Cache hit for encrypted session key " + Hex.toHexString(secKeyData[0])); } return sessionKey; } - private byte[] lookup(byte[][] secKeyData) { + public byte[] costlyRecoverSessionData(int keyAlgorithm, byte[][] secKeyData) throws PGPException { + return super.recoverSessionData(keyAlgorithm, secKeyData); + } + + private byte[] lookupSessionKeyData(byte[][] secKeyData) { byte[] sk = secKeyData[0]; String key = Base64.toBase64String(sk); byte[] sessionKey = cachedSessionKeys.get(key); return copy(sessionKey); } - private void cache(byte[][] secKeyData, byte[] sessionKey) { + private void cacheSessionKeyData(byte[][] secKeyData, byte[] sessionKey) { byte[] sk = secKeyData[0]; String key = Base64.toBase64String(sk); cachedSessionKeys.put(key, copy(sessionKey)); @@ -69,7 +85,7 @@ public class CachingPublicKeyDataDecryptorFactory implements PublicKeyDataDecryp } @Override - public PGPDataDecryptor createDataDecryptor(boolean withIntegrityPacket, int encAlgorithm, byte[] key) throws PGPException { - return null; + public SubkeyIdentifier getSubkeyIdentifier() { + return decryptionKey; } } diff --git a/pgpainless-core/src/test/java/bouncycastle/CachingBcPublicKeyDataDecryptorFactoryTest.java b/pgpainless-core/src/test/java/bouncycastle/CachingBcPublicKeyDataDecryptorFactoryTest.java new file mode 100644 index 00000000..186bcbc2 --- /dev/null +++ b/pgpainless-core/src/test/java/bouncycastle/CachingBcPublicKeyDataDecryptorFactoryTest.java @@ -0,0 +1,96 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package bouncycastle; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.security.InvalidAlgorithmParameterException; +import java.security.NoSuchAlgorithmException; + +import org.bouncycastle.CachingBcPublicKeyDataDecryptorFactory; +import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.PGPPrivateKey; +import org.bouncycastle.openpgp.PGPSecretKey; +import org.bouncycastle.openpgp.PGPSecretKeyRing; +import org.bouncycastle.util.io.Streams; +import org.junit.jupiter.api.Test; +import org.pgpainless.PGPainless; +import org.pgpainless.algorithm.EncryptionPurpose; +import org.pgpainless.decryption_verification.ConsumerOptions; +import org.pgpainless.decryption_verification.DecryptionStream; +import org.pgpainless.key.SubkeyIdentifier; +import org.pgpainless.key.info.KeyRingInfo; +import org.pgpainless.key.protection.SecretKeyRingProtector; +import org.pgpainless.key.protection.UnlockSecretKey; + +public class CachingBcPublicKeyDataDecryptorFactoryTest { + + private static final String KEY = "-----BEGIN PGP PRIVATE KEY BLOCK-----\n" + + "Version: PGPainless\n" + + "Comment: C8AE 4279 5958 5F46 86A9 8B5F EC69 7C29 2BE4 44E0\n" + + "Comment: Alice\n" + + "\n" + + "lFgEY1vEcxYJKwYBBAHaRw8BAQdAXOUK1uc1iBeM+mMt2nLCukXWoJd/SodrtN9S\n" + + "U/zzwu0AAP9eePPw91KLuq6PF9jQoTRz/cW4CyiALNJpsOJIZ1rp3xOBtAVBbGlj\n" + + "ZYiPBBMWCgBBBQJjW8RzCRDsaXwpK+RE4BYhBMiuQnlZWF9GhqmLX+xpfCkr5ETg\n" + + "Ap4BApsBBRYCAwEABAsJCAcFFQoJCAsCmQEAAGqWAQC8oz7l8izjUis5ji+sgI+q\n" + + "gML22VNybqmLBpzZwnNU5wEApe9fNTRbK5yAITGBscxH7o74Qe+CLI6Ni5MwzKxr\n" + + "5AucXQRjW8RzEgorBgEEAZdVAQUBAQdAm8xk0QSvpp2ZU1KQ31E7eEZYLKpbW4JE\n" + + "opmtMQx6AlIDAQgHAAD/XTb/qSosfkNvli3BQiUzVRAqKaU4PKAq7at6afxoYSgN\n" + + "4Yh1BBgWCgAdBQJjW8RzAp4BApsMBRYCAwEABAsJCAcFFQoJCAsACgkQ7Gl8KSvk\n" + + "ROB38QEA0MvDt0bjEXwFoM0E34z0MtPcG3VBYcQ+iFRIqFfEl5UA/2yZxFjoZqrs\n" + + "AQE8TaVpXYfbc2p/GEKA9LGd9l/g0QQLnFgEY1vEcxYJKwYBBAHaRw8BAQdAyCOv\n" + + "6hGUvHcCBSDKP3fRz+scyJ9zwMt7nFXK5A/k2YgAAQCn3Es+IhvePn3eBlcYMMr0\n" + + "xcktrY1NJAIZPfjlUJ0J1g6LiNUEGBYKAH0FAmNbxHMCngECmwIFFgIDAQAECwkI\n" + + "BwUVCgkIC18gBBkWCgAGBQJjW8RzAAoJECxLf7KoUc8wD18BANNpIr4E+RRVVztR\n" + + "OVwdxSe0SRWGjkW8nHrRyghHKTuMAP9p4ZKicOYA1uZbiNNjyuJuS8xBH6Hihurb\n" + + "gDypVgxdBQAKCRDsaXwpK+RE4EQjAP9ARZEPxKNLFkrvjoZ8nrts3qhv3VtMrU+9\n" + + "huZnYLe1FQEAtgO6V7wutHvVARHXqPJ6lcv+SueIu+BjLFYEKuBwggs=\n" + + "=ShJd\n" + + "-----END PGP PRIVATE KEY BLOCK-----"; + + private static final String MSG = "-----BEGIN PGP MESSAGE-----\n" + + "Version: PGPainless\n" + + "\n" + + "hF4DJmQMTBqw3G8SAQdALkHpO0UkS/CqkwxUz74MJU3PV72ZrIL8ZcrO8ofhblkw\n" + + "iDIhSwwGTG3tj+sG+ZVWKsmONKi7Om5seJDHQtQ8MfdCELAgwYHSt6MrgDBhuDIH\n" + + "0kABZhq2/8qk3EGXPpc+xxs4r4g8SgHOiiHSim5NGtounXXIaF6T/hUmlorkeYf/\n" + + "a9pCC0QXRUAr8NOcdsfbvb5V\n" + + "=dQa8\n" + + "-----END PGP MESSAGE-----"; + + @Test + public void test() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, IOException { + PGPSecretKeyRing secretKeys = PGPainless.readKeyRing().secretKeyRing(KEY); + SecretKeyRingProtector protector = SecretKeyRingProtector.unprotectedKeys(); + KeyRingInfo info = PGPainless.inspectKeyRing(secretKeys); + SubkeyIdentifier decryptionKey = new SubkeyIdentifier(secretKeys, + info.getEncryptionSubkeys(EncryptionPurpose.ANY).get(0).getKeyID()); + + PGPSecretKey secretKey = secretKeys.getSecretKey(decryptionKey.getSubkeyId()); + PGPPrivateKey privateKey = UnlockSecretKey.unlockSecretKey(secretKey, protector); + CachingBcPublicKeyDataDecryptorFactory cachingFactory = new CachingBcPublicKeyDataDecryptorFactory( + privateKey, decryptionKey); + + ByteArrayInputStream ciphertextIn = new ByteArrayInputStream(MSG.getBytes()); + DecryptionStream decryptionStream = PGPainless.decryptAndOrVerify() + .onInputStream(ciphertextIn) + .withOptions(ConsumerOptions.get() + .addCustomDecryptorFactory(cachingFactory)); + + ByteArrayOutputStream out = new ByteArrayOutputStream(); + Streams.pipeAll(decryptionStream, out); + decryptionStream.close(); + + decryptionStream = PGPainless.decryptAndOrVerify() + .onInputStream(ciphertextIn) + .withOptions(ConsumerOptions.get() + .addCustomDecryptorFactory(cachingFactory)); + out = new ByteArrayOutputStream(); + Streams.pipeAll(decryptionStream, out); + decryptionStream.close(); + } +} From 07320ed3cfc4ab3cf6158a9e2ea549b5166f0372 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Fri, 28 Oct 2022 14:56:41 +0200 Subject: [PATCH 080/763] Fix HardwareSecurity.getIdsOfHardwareBackedKeys() --- .../decryption_verification/HardwareSecurity.java | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/HardwareSecurity.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/HardwareSecurity.java index f234ff00..6d9719dd 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/HardwareSecurity.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/HardwareSecurity.java @@ -47,8 +47,8 @@ public class HardwareSecurity { * @param secretKeys secret keys * @return set of keys with S2K type DIVERT_TO_CARD or GNU_DUMMY_S2K */ - public static Set getIdsOfHardwareBackedKeys(PGPSecretKeyRing secretKeys) { - Set hardwareBackedKeys = new HashSet<>(); + public static Set getIdsOfHardwareBackedKeys(PGPSecretKeyRing secretKeys) { + Set hardwareBackedKeys = new HashSet<>(); for (PGPSecretKey secretKey : secretKeys) { S2K s2K = secretKey.getS2K(); if (s2K == null) { @@ -56,9 +56,11 @@ public class HardwareSecurity { } int type = s2K.getType(); + int mode = s2K.getProtectionMode(); // TODO: Is GNU_DUMMY_S2K appropriate? - if (type == S2K.GNU_PROTECTION_MODE_DIVERT_TO_CARD || type == S2K.GNU_DUMMY_S2K) { - hardwareBackedKeys.add(secretKey.getKeyID()); + if (type == S2K.GNU_DUMMY_S2K && mode == S2K.GNU_PROTECTION_MODE_DIVERT_TO_CARD) { + SubkeyIdentifier hardwareBackedKey = new SubkeyIdentifier(secretKeys, secretKey.getKeyID()); + hardwareBackedKeys.add(hardwareBackedKey); } } return hardwareBackedKeys; @@ -75,7 +77,7 @@ public class HardwareSecurity { // luckily we can instantiate the BcPublicKeyDataDecryptorFactory with null as argument. private final PublicKeyDataDecryptorFactory factory = new BcPublicKeyDataDecryptorFactory(null); - private SubkeyIdentifier subkey; + private final SubkeyIdentifier subkey; /** * Create a new {@link HardwareDataDecryptorFactory}. From 7467170bccef1ec52b16fe389281f042a9c531b7 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Fri, 28 Oct 2022 16:20:36 +0200 Subject: [PATCH 081/763] Move CachingBcPublicKeyDataDecryptorFactoryTest to correct package --- .../CachingBcPublicKeyDataDecryptorFactoryTest.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) rename pgpainless-core/src/test/java/{ => org}/bouncycastle/CachingBcPublicKeyDataDecryptorFactoryTest.java (98%) diff --git a/pgpainless-core/src/test/java/bouncycastle/CachingBcPublicKeyDataDecryptorFactoryTest.java b/pgpainless-core/src/test/java/org/bouncycastle/CachingBcPublicKeyDataDecryptorFactoryTest.java similarity index 98% rename from pgpainless-core/src/test/java/bouncycastle/CachingBcPublicKeyDataDecryptorFactoryTest.java rename to pgpainless-core/src/test/java/org/bouncycastle/CachingBcPublicKeyDataDecryptorFactoryTest.java index 186bcbc2..6a43772d 100644 --- a/pgpainless-core/src/test/java/bouncycastle/CachingBcPublicKeyDataDecryptorFactoryTest.java +++ b/pgpainless-core/src/test/java/org/bouncycastle/CachingBcPublicKeyDataDecryptorFactoryTest.java @@ -2,7 +2,7 @@ // // SPDX-License-Identifier: Apache-2.0 -package bouncycastle; +package org.bouncycastle; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; @@ -10,7 +10,6 @@ import java.io.IOException; import java.security.InvalidAlgorithmParameterException; import java.security.NoSuchAlgorithmException; -import org.bouncycastle.CachingBcPublicKeyDataDecryptorFactory; import org.bouncycastle.openpgp.PGPException; import org.bouncycastle.openpgp.PGPPrivateKey; import org.bouncycastle.openpgp.PGPSecretKey; From 2487e3300a58f1ed7e615f54f8c9dd0549a76e00 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Fri, 28 Oct 2022 16:36:08 +0200 Subject: [PATCH 082/763] Add and test GnuDummyKeyUtil --- .../key/gnu_dummy_s2k/GNUExtension.java | 24 +++ .../key/gnu_dummy_s2k/GnuDummyKeyUtil.java | 114 ++++++++++ .../key/gnu_dummy_s2k/package-info.java | 8 + .../HardwareSecurityTest.java | 82 ++++++++ .../gnu_dummy_s2k/GnuDummyKeyUtilTest.java | 199 ++++++++++++++++++ 5 files changed, 427 insertions(+) create mode 100644 pgpainless-core/src/main/java/org/pgpainless/key/gnu_dummy_s2k/GNUExtension.java create mode 100644 pgpainless-core/src/main/java/org/pgpainless/key/gnu_dummy_s2k/GnuDummyKeyUtil.java create mode 100644 pgpainless-core/src/main/java/org/pgpainless/key/gnu_dummy_s2k/package-info.java create mode 100644 pgpainless-core/src/test/java/org/pgpainless/decryption_verification/HardwareSecurityTest.java create mode 100644 pgpainless-core/src/test/java/org/pgpainless/key/gnu_dummy_s2k/GnuDummyKeyUtilTest.java diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/gnu_dummy_s2k/GNUExtension.java b/pgpainless-core/src/main/java/org/pgpainless/key/gnu_dummy_s2k/GNUExtension.java new file mode 100644 index 00000000..ff42ef6e --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/key/gnu_dummy_s2k/GNUExtension.java @@ -0,0 +1,24 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.key.gnu_dummy_s2k; + +import org.bouncycastle.bcpg.S2K; + +public enum GNUExtension { + + NO_PRIVATE_KEY(S2K.GNU_PROTECTION_MODE_NO_PRIVATE_KEY), + DIVERT_TO_CARD(S2K.GNU_PROTECTION_MODE_DIVERT_TO_CARD), + ; + + private final int id; + + GNUExtension(int id) { + this.id = id; + } + + public int getId() { + return id; + } +} diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/gnu_dummy_s2k/GnuDummyKeyUtil.java b/pgpainless-core/src/main/java/org/pgpainless/key/gnu_dummy_s2k/GnuDummyKeyUtil.java new file mode 100644 index 00000000..a8de5aa3 --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/key/gnu_dummy_s2k/GnuDummyKeyUtil.java @@ -0,0 +1,114 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.key.gnu_dummy_s2k; + +import org.bouncycastle.bcpg.PublicKeyPacket; +import org.bouncycastle.bcpg.S2K; +import org.bouncycastle.bcpg.SecretKeyPacket; +import org.bouncycastle.bcpg.SecretSubkeyPacket; +import org.bouncycastle.openpgp.PGPSecretKey; +import org.bouncycastle.openpgp.PGPSecretKeyRing; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +public final class GnuDummyKeyUtil { + + private GnuDummyKeyUtil() { + + } + + public static Builder modify(PGPSecretKeyRing secretKeys) { + return new Builder(secretKeys); + } + + public static final class Builder { + + private final PGPSecretKeyRing keys; + + private Builder(PGPSecretKeyRing keys) { + this.keys = keys; + } + + public PGPSecretKeyRing removePrivateKeys(KeyFilter filter) { + return doIt(GNUExtension.NO_PRIVATE_KEY, null, filter); + } + + public PGPSecretKeyRing divertPrivateKeysToCard(KeyFilter filter) { + return divertPrivateKeysToCard(filter, new byte[16]); + } + + public PGPSecretKeyRing divertPrivateKeysToCard(KeyFilter filter, byte[] cardSerialNumber) { + return doIt(GNUExtension.DIVERT_TO_CARD, cardSerialNumber, filter); + } + + private PGPSecretKeyRing doIt(GNUExtension extension, byte[] serial, KeyFilter filter) { + byte[] encodedSerial = serial != null ? encodeSerial(serial) : null; + S2K s2k = extensionToS2K(extension); + + List secretKeyList = new ArrayList<>(); + for (PGPSecretKey secretKey : keys) { + if (!filter.filter(secretKey.getKeyID())) { + // No conversion, do not modify subkey + secretKeyList.add(secretKey); + continue; + } + + PublicKeyPacket publicKeyPacket = secretKey.getPublicKey().getPublicKeyPacket(); + if (secretKey.isMasterKey()) { + SecretKeyPacket keyPacket = new SecretKeyPacket(publicKeyPacket, + 0, 255, s2k, null, encodedSerial); + PGPSecretKey onCard = new PGPSecretKey(keyPacket, secretKey.getPublicKey()); + secretKeyList.add(onCard); + } else { + SecretSubkeyPacket keyPacket = new SecretSubkeyPacket(publicKeyPacket, + 0, 255, s2k, null, encodedSerial); + PGPSecretKey onCard = new PGPSecretKey(keyPacket, secretKey.getPublicKey()); + secretKeyList.add(onCard); + } + } + + PGPSecretKeyRing gnuDummyKey = new PGPSecretKeyRing(secretKeyList); + return gnuDummyKey; + } + + private byte[] encodeSerial(byte[] serial) { + byte[] encoded = new byte[serial.length + 1]; + encoded[0] = 0x10; + System.arraycopy(serial, 0, encoded, 1, serial.length); + return encoded; + } + + private S2K extensionToS2K(GNUExtension extension) { + S2K s2k = S2K.gnuDummyS2K(extension == GNUExtension.DIVERT_TO_CARD ? + S2K.GNUDummyParams.divertToCard() : S2K.GNUDummyParams.noPrivateKey()); + return s2k; + } + } + + public interface KeyFilter { + + /** + * Return true, if the given key should be selected, false otherwise. + * + * @param keyId id of the key + * @return select + */ + boolean filter(long keyId); + + static KeyFilter any() { + return keyId -> true; + } + + static KeyFilter only(long onlyKeyId) { + return keyId -> keyId == onlyKeyId; + } + + static KeyFilter selected(Collection ids) { + return keyId -> ids.contains(keyId); + } + } +} diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/gnu_dummy_s2k/package-info.java b/pgpainless-core/src/main/java/org/pgpainless/key/gnu_dummy_s2k/package-info.java new file mode 100644 index 00000000..5c2a727e --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/key/gnu_dummy_s2k/package-info.java @@ -0,0 +1,8 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +/** + * Utility classes related to creating keys with GNU DUMMY S2K values. + */ +package org.pgpainless.key.gnu_dummy_s2k; diff --git a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/HardwareSecurityTest.java b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/HardwareSecurityTest.java new file mode 100644 index 00000000..f1606cec --- /dev/null +++ b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/HardwareSecurityTest.java @@ -0,0 +1,82 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.decryption_verification; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.IOException; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; + +import org.bouncycastle.openpgp.PGPSecretKey; +import org.bouncycastle.openpgp.PGPSecretKeyRing; +import org.junit.jupiter.api.Test; +import org.pgpainless.PGPainless; +import org.pgpainless.key.SubkeyIdentifier; +import org.pgpainless.key.gnu_dummy_s2k.GnuDummyKeyUtil; +import org.pgpainless.key.util.KeyIdUtil; + +public class HardwareSecurityTest { + + private static final String KEY = "-----BEGIN PGP PRIVATE KEY BLOCK-----\n" + + "Version: PGPainless\n" + + "Comment: DE2E 9AB2 6650 8191 53E7 D599 C176 507F 2B5D 43B3\n" + + "Comment: Alice \n" + + "\n" + + "lFgEY1vjgRYJKwYBBAHaRw8BAQdAXjLoPTOIOdvlFT2Nt3rcvLTVx5ujPBGghZ5S\n" + + "D5tEnyoAAP0fAUJTiPrxZYdzs6MP0KFo+Nmr/wb1PJHTkzmYpt4wkRKBtBxBbGlj\n" + + "ZSA8YWxpY2VAcGdwYWlubGVzcy5vcmc+iI8EExYKAEEFAmNb44EJEMF2UH8rXUOz\n" + + "FiEE3i6asmZQgZFT59WZwXZQfytdQ7MCngECmwEFFgIDAQAECwkIBwUVCgkICwKZ\n" + + "AQAAHLYA/AgW+YrpU+UqrwX2dhY6RAfgHTTMU89RHjaTHJx8pLrBAP4gthGof00a\n" + + "XEjwTWteDOO049SIp2AUfj9deJqtrQcHD5xdBGNb44ESCisGAQQBl1UBBQEBB0DN\n" + + "vUT3awa3YLmwf41LRpPrm7B87AOHfYIP8S9QJ4GDJgMBCAcAAP9bwlSaF+lti8JY\n" + + "qKFO3qt3ZYQMu1l/LRBle89ZB4zD+BDOiHUEGBYKAB0FAmNb44ECngECmwwFFgID\n" + + "AQAECwkIBwUVCgkICwAKCRDBdlB/K11Ds/TsAP9kvpUrCWnrWGq+a9n1CqEfCMX5\n" + + "cT+qzrwNf+J0L22KowD+M9SVO0qssiAqutLE9h9dGYLbEiFvsHzK3WSnjKYbIgac\n" + + "WARjW+OBFgkrBgEEAdpHDwEBB0BCPh8M5TnXSmG6Ygwp4j5RR4u3hmxl8CYjX4h/\n" + + "XtvvNwAA/RP04coSrLHVI6vUfbJk4MhWYeyhJBRYY0vGp7yq+wVtEpKI1QQYFgoA\n" + + "fQUCY1vjgQKeAQKbAgUWAgMBAAQLCQgHBRUKCQgLXyAEGRYKAAYFAmNb44EACgkQ\n" + + "mlozJSF7rXQW+AD/TA3YBxTd+YbBSwfgqzWNbfT9BBcFrdn3uPCsbvfmqXoA/3oj\n" + + "oupkgoaXesrGxn2k9hW9/GBXSvNcgY2txZ6/oYoIAAoJEMF2UH8rXUOziZ4A/0Xl\n" + + "xSZJWmkRpBh5AO8Cnqosz6j947IYAxS16ay+sIOHAP9aN9CUNJIIdHnHdFHO4GZz\n" + + "ejjknn4wt8NVJP97JxlnBQ==\n" + + "=qSQb\n" + + "-----END PGP PRIVATE KEY BLOCK-----"; + + @Test + public void testGetSingleIdOfHardwareBackedKey() throws IOException { + PGPSecretKeyRing secretKeys = PGPainless.readKeyRing().secretKeyRing(KEY); + assertTrue(HardwareSecurity.getIdsOfHardwareBackedKeys(secretKeys).isEmpty()); + long encryptionKeyId = KeyIdUtil.fromLongKeyId("0AAD8F5891262F50"); + + PGPSecretKeyRing withHardwareBackedEncryptionKey = GnuDummyKeyUtil.modify(secretKeys) + .divertPrivateKeysToCard(GnuDummyKeyUtil.KeyFilter.only(encryptionKeyId)); + + Set hardwareBackedKeys = HardwareSecurity + .getIdsOfHardwareBackedKeys(withHardwareBackedEncryptionKey); + assertEquals(Collections.singleton(new SubkeyIdentifier(secretKeys, encryptionKeyId)), hardwareBackedKeys); + } + + + @Test + public void testGetIdsOfFullyHardwareBackedKey() throws IOException { + PGPSecretKeyRing secretKeys = PGPainless.readKeyRing().secretKeyRing(KEY); + assertTrue(HardwareSecurity.getIdsOfHardwareBackedKeys(secretKeys).isEmpty()); + + PGPSecretKeyRing withHardwareBackedEncryptionKey = GnuDummyKeyUtil.modify(secretKeys) + .divertPrivateKeysToCard(GnuDummyKeyUtil.KeyFilter.any()); + Set expected = new HashSet<>(); + for (PGPSecretKey key : secretKeys) { + expected.add(new SubkeyIdentifier(secretKeys, key.getKeyID())); + } + + Set hardwareBackedKeys = HardwareSecurity + .getIdsOfHardwareBackedKeys(withHardwareBackedEncryptionKey); + + assertEquals(expected, hardwareBackedKeys); + } +} diff --git a/pgpainless-core/src/test/java/org/pgpainless/key/gnu_dummy_s2k/GnuDummyKeyUtilTest.java b/pgpainless-core/src/test/java/org/pgpainless/key/gnu_dummy_s2k/GnuDummyKeyUtilTest.java new file mode 100644 index 00000000..0b19c551 --- /dev/null +++ b/pgpainless-core/src/test/java/org/pgpainless/key/gnu_dummy_s2k/GnuDummyKeyUtilTest.java @@ -0,0 +1,199 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.key.gnu_dummy_s2k; + +import org.bouncycastle.bcpg.S2K; +import org.bouncycastle.openpgp.PGPSecretKey; +import org.bouncycastle.openpgp.PGPSecretKeyRing; +import org.junit.jupiter.api.Test; +import org.pgpainless.PGPainless; +import org.pgpainless.key.util.KeyIdUtil; + +import java.io.IOException; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class GnuDummyKeyUtilTest { + // normal, non-hw-backed key + private static final String FULL_KEY = "-----BEGIN PGP PRIVATE KEY BLOCK-----\n" + + "Version: PGPainless\n" + + "Comment: 01FD AB6C E04A 5078 79FE 4A18 C312 C97D A9F7 6A4F\n" + + "Comment: Hardy Hardware \n" + + "\n" + + "lFgEY1vSiBYJKwYBBAHaRw8BAQdAQ58lZn/HOtg+1b1KS18odyQ6M4LaDdbJAyRf\n" + + "eBwCeTQAAPwJN+Xmr0jjN7RA9jgqXnxC/rcWHmdp/j9NdEd7K2Wbxw/rtCBIYXJk\n" + + "eSBIYXJkd2FyZSA8aGFyZHlAaGFyZC53YXJlPoiPBBMWCgBBBQJjW9KICRDDEsl9\n" + + "qfdqTxYhBAH9q2zgSlB4ef5KGMMSyX2p92pPAp4BApsBBRYCAwEABAsJCAcFFQoJ\n" + + "CAsCmQEAAPk2AP922T5TQ7hukFlpxX3ThMhieJnECGY5Eqt5U0/vEY1XdgD/eE1M\n" + + "l9qqx6QGcaNKe8deMe3EhTant6mS9tqMHp2/3gmcXQRjW9KIEgorBgEEAZdVAQUB\n" + + "AQdAVXBLNvNmFh9KX6iLmdNJM28Zc9PGnzEoAD9+T4p0lDwDAQgHAAD/fw9hnzeH\n" + + "VtBaHi6efXvnc4rdVj8zWk0LKo1clFd3bTAN+oh1BBgWCgAdBQJjW9KIAp4BApsM\n" + + "BRYCAwEABAsJCAcFFQoJCAsACgkQwxLJfan3ak/JyQD9GBj0vjtYZAf5Fi0eEKdi\n" + + "Ags0yZrQPkMs6eL+83te770A/jG0DeJy+88fOfWTj+mixO98PZPnQ0MybWC/1QUT\n" + + "vP0BnFgEY1vSiBYJKwYBBAHaRw8BAQdAvSYTD60t8vx10dSEBACUoIfVCpeOB30D\n" + + "6nfwJtbDT0YAAQCgnCsN9iX7s2TQd8NPggWs4QdhaFpb6olt3SlAvUy/wRBDiNUE\n" + + "GBYKAH0FAmNb0ogCngECmwIFFgIDAQAECwkIBwUVCgkIC18gBBkWCgAGBQJjW9KI\n" + + "AAoJEJQCL6VtwFtJDmMBAKqsGfRFQxJXyPgugWBgEaO5lt9fMM0yUxa76cmSWe5f\n" + + "AQD2oLSEW1GOgIs64+Z3gvtXopmeupT09HhI7ger98zDAwAKCRDDEsl9qfdqTwR6\n" + + "AP9Xftw8xZ7/MWhYImk/xheqPy07K4qo3T1pGKUvUqjWQQEAhE3r0oTcJn+KVCwG\n" + + "jF6AYiLOzO/R1x5bSlYD3FeJ3Qo=\n" + + "=+vXp\n" + + "-----END PGP PRIVATE KEY BLOCK-----\n"; + + private static final long primaryKeyId = KeyIdUtil.fromLongKeyId("C312C97DA9F76A4F"); + private static final long encryptionKeyId = KeyIdUtil.fromLongKeyId("6924D066714CE8C6"); + private static final long signatureKeyId = KeyIdUtil.fromLongKeyId("94022FA56DC05B49"); + private static final byte[] cardSerial = new byte[] {0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f}; + + public static final String ALL_KEYS_ON_CARD = "-----BEGIN PGP PRIVATE KEY BLOCK-----\n" + + "Version: PGPainless\n" + + "Comment: 01FD AB6C E04A 5078 79FE 4A18 C312 C97D A9F7 6A4F\n" + + "Comment: Hardy Hardware \n" + + "\n" + + "lEwEY1vSiBYJKwYBBAHaRw8BAQdAQ58lZn/HOtg+1b1KS18odyQ6M4LaDdbJAyRf\n" + + "eBwCeTT/AGUAR05VAhAAAQIDBAUGBwgJCgsMDQ4PtCBIYXJkeSBIYXJkd2FyZSA8\n" + + "aGFyZHlAaGFyZC53YXJlPoiPBBMWCgBBBQJjW9KICRDDEsl9qfdqTxYhBAH9q2zg\n" + + "SlB4ef5KGMMSyX2p92pPAp4BApsBBRYCAwEABAsJCAcFFQoJCAsCmQEAAPk2AP92\n" + + "2T5TQ7hukFlpxX3ThMhieJnECGY5Eqt5U0/vEY1XdgD/eE1Ml9qqx6QGcaNKe8de\n" + + "Me3EhTant6mS9tqMHp2/3gmcUQRjW9KIEgorBgEEAZdVAQUBAQdAVXBLNvNmFh9K\n" + + "X6iLmdNJM28Zc9PGnzEoAD9+T4p0lDwDAQgH/wBlAEdOVQIQAAECAwQFBgcICQoL\n" + + "DA0OD4h1BBgWCgAdBQJjW9KIAp4BApsMBRYCAwEABAsJCAcFFQoJCAsACgkQwxLJ\n" + + "fan3ak/JyQD9GBj0vjtYZAf5Fi0eEKdiAgs0yZrQPkMs6eL+83te770A/jG0DeJy\n" + + "+88fOfWTj+mixO98PZPnQ0MybWC/1QUTvP0BnEwEY1vSiBYJKwYBBAHaRw8BAQdA\n" + + "vSYTD60t8vx10dSEBACUoIfVCpeOB30D6nfwJtbDT0b/AGUAR05VAhAAAQIDBAUG\n" + + "BwgJCgsMDQ4PiNUEGBYKAH0FAmNb0ogCngECmwIFFgIDAQAECwkIBwUVCgkIC18g\n" + + "BBkWCgAGBQJjW9KIAAoJEJQCL6VtwFtJDmMBAKqsGfRFQxJXyPgugWBgEaO5lt9f\n" + + "MM0yUxa76cmSWe5fAQD2oLSEW1GOgIs64+Z3gvtXopmeupT09HhI7ger98zDAwAK\n" + + "CRDDEsl9qfdqTwR6AP9Xftw8xZ7/MWhYImk/xheqPy07K4qo3T1pGKUvUqjWQQEA\n" + + "hE3r0oTcJn+KVCwGjF6AYiLOzO/R1x5bSlYD3FeJ3Qo=\n" + + "=wsFa\n" + + "-----END PGP PRIVATE KEY BLOCK-----"; + + public static final String PRIMARY_KEY_ON_CARD = "-----BEGIN PGP PRIVATE KEY BLOCK-----\n" + + "Version: PGPainless\n" + + "Comment: 01FD AB6C E04A 5078 79FE 4A18 C312 C97D A9F7 6A4F\n" + + "Comment: Hardy Hardware \n" + + "\n" + + "lEwEY1vSiBYJKwYBBAHaRw8BAQdAQ58lZn/HOtg+1b1KS18odyQ6M4LaDdbJAyRf\n" + + "eBwCeTT/AGUAR05VAhAAAQIDBAUGBwgJCgsMDQ4PtCBIYXJkeSBIYXJkd2FyZSA8\n" + + "aGFyZHlAaGFyZC53YXJlPoiPBBMWCgBBBQJjW9KICRDDEsl9qfdqTxYhBAH9q2zg\n" + + "SlB4ef5KGMMSyX2p92pPAp4BApsBBRYCAwEABAsJCAcFFQoJCAsCmQEAAPk2AP92\n" + + "2T5TQ7hukFlpxX3ThMhieJnECGY5Eqt5U0/vEY1XdgD/eE1Ml9qqx6QGcaNKe8de\n" + + "Me3EhTant6mS9tqMHp2/3gmcXQRjW9KIEgorBgEEAZdVAQUBAQdAVXBLNvNmFh9K\n" + + "X6iLmdNJM28Zc9PGnzEoAD9+T4p0lDwDAQgHAAD/fw9hnzeHVtBaHi6efXvnc4rd\n" + + "Vj8zWk0LKo1clFd3bTAN+oh1BBgWCgAdBQJjW9KIAp4BApsMBRYCAwEABAsJCAcF\n" + + "FQoJCAsACgkQwxLJfan3ak/JyQD9GBj0vjtYZAf5Fi0eEKdiAgs0yZrQPkMs6eL+\n" + + "83te770A/jG0DeJy+88fOfWTj+mixO98PZPnQ0MybWC/1QUTvP0BnFgEY1vSiBYJ\n" + + "KwYBBAHaRw8BAQdAvSYTD60t8vx10dSEBACUoIfVCpeOB30D6nfwJtbDT0YAAQCg\n" + + "nCsN9iX7s2TQd8NPggWs4QdhaFpb6olt3SlAvUy/wRBDiNUEGBYKAH0FAmNb0ogC\n" + + "ngECmwIFFgIDAQAECwkIBwUVCgkIC18gBBkWCgAGBQJjW9KIAAoJEJQCL6VtwFtJ\n" + + "DmMBAKqsGfRFQxJXyPgugWBgEaO5lt9fMM0yUxa76cmSWe5fAQD2oLSEW1GOgIs6\n" + + "4+Z3gvtXopmeupT09HhI7ger98zDAwAKCRDDEsl9qfdqTwR6AP9Xftw8xZ7/MWhY\n" + + "Imk/xheqPy07K4qo3T1pGKUvUqjWQQEAhE3r0oTcJn+KVCwGjF6AYiLOzO/R1x5b\n" + + "SlYD3FeJ3Qo=\n" + + "=s+B1\n" + + "-----END PGP PRIVATE KEY BLOCK-----"; + + public static final String ENCRYPTION_KEY_ON_CARD = "-----BEGIN PGP PRIVATE KEY BLOCK-----\n" + + "Version: PGPainless\n" + + "Comment: 01FD AB6C E04A 5078 79FE 4A18 C312 C97D A9F7 6A4F\n" + + "Comment: Hardy Hardware \n" + + "\n" + + "lFgEY1vSiBYJKwYBBAHaRw8BAQdAQ58lZn/HOtg+1b1KS18odyQ6M4LaDdbJAyRf\n" + + "eBwCeTQAAPwJN+Xmr0jjN7RA9jgqXnxC/rcWHmdp/j9NdEd7K2Wbxw/rtCBIYXJk\n" + + "eSBIYXJkd2FyZSA8aGFyZHlAaGFyZC53YXJlPoiPBBMWCgBBBQJjW9KICRDDEsl9\n" + + "qfdqTxYhBAH9q2zgSlB4ef5KGMMSyX2p92pPAp4BApsBBRYCAwEABAsJCAcFFQoJ\n" + + "CAsCmQEAAPk2AP922T5TQ7hukFlpxX3ThMhieJnECGY5Eqt5U0/vEY1XdgD/eE1M\n" + + "l9qqx6QGcaNKe8deMe3EhTant6mS9tqMHp2/3gmcUQRjW9KIEgorBgEEAZdVAQUB\n" + + "AQdAVXBLNvNmFh9KX6iLmdNJM28Zc9PGnzEoAD9+T4p0lDwDAQgH/wBlAEdOVQIQ\n" + + "AAECAwQFBgcICQoLDA0OD4h1BBgWCgAdBQJjW9KIAp4BApsMBRYCAwEABAsJCAcF\n" + + "FQoJCAsACgkQwxLJfan3ak/JyQD9GBj0vjtYZAf5Fi0eEKdiAgs0yZrQPkMs6eL+\n" + + "83te770A/jG0DeJy+88fOfWTj+mixO98PZPnQ0MybWC/1QUTvP0BnFgEY1vSiBYJ\n" + + "KwYBBAHaRw8BAQdAvSYTD60t8vx10dSEBACUoIfVCpeOB30D6nfwJtbDT0YAAQCg\n" + + "nCsN9iX7s2TQd8NPggWs4QdhaFpb6olt3SlAvUy/wRBDiNUEGBYKAH0FAmNb0ogC\n" + + "ngECmwIFFgIDAQAECwkIBwUVCgkIC18gBBkWCgAGBQJjW9KIAAoJEJQCL6VtwFtJ\n" + + "DmMBAKqsGfRFQxJXyPgugWBgEaO5lt9fMM0yUxa76cmSWe5fAQD2oLSEW1GOgIs6\n" + + "4+Z3gvtXopmeupT09HhI7ger98zDAwAKCRDDEsl9qfdqTwR6AP9Xftw8xZ7/MWhY\n" + + "Imk/xheqPy07K4qo3T1pGKUvUqjWQQEAhE3r0oTcJn+KVCwGjF6AYiLOzO/R1x5b\n" + + "SlYD3FeJ3Qo=\n" + + "=TPAl\n" + + "-----END PGP PRIVATE KEY BLOCK-----"; + + public static final String SIGNATURE_KEY_ON_CARD = "-----BEGIN PGP PRIVATE KEY BLOCK-----\n" + + "Version: PGPainless\n" + + "Comment: 01FD AB6C E04A 5078 79FE 4A18 C312 C97D A9F7 6A4F\n" + + "Comment: Hardy Hardware \n" + + "\n" + + "lFgEY1vSiBYJKwYBBAHaRw8BAQdAQ58lZn/HOtg+1b1KS18odyQ6M4LaDdbJAyRf\n" + + "eBwCeTQAAPwJN+Xmr0jjN7RA9jgqXnxC/rcWHmdp/j9NdEd7K2Wbxw/rtCBIYXJk\n" + + "eSBIYXJkd2FyZSA8aGFyZHlAaGFyZC53YXJlPoiPBBMWCgBBBQJjW9KICRDDEsl9\n" + + "qfdqTxYhBAH9q2zgSlB4ef5KGMMSyX2p92pPAp4BApsBBRYCAwEABAsJCAcFFQoJ\n" + + "CAsCmQEAAPk2AP922T5TQ7hukFlpxX3ThMhieJnECGY5Eqt5U0/vEY1XdgD/eE1M\n" + + "l9qqx6QGcaNKe8deMe3EhTant6mS9tqMHp2/3gmcXQRjW9KIEgorBgEEAZdVAQUB\n" + + "AQdAVXBLNvNmFh9KX6iLmdNJM28Zc9PGnzEoAD9+T4p0lDwDAQgHAAD/fw9hnzeH\n" + + "VtBaHi6efXvnc4rdVj8zWk0LKo1clFd3bTAN+oh1BBgWCgAdBQJjW9KIAp4BApsM\n" + + "BRYCAwEABAsJCAcFFQoJCAsACgkQwxLJfan3ak/JyQD9GBj0vjtYZAf5Fi0eEKdi\n" + + "Ags0yZrQPkMs6eL+83te770A/jG0DeJy+88fOfWTj+mixO98PZPnQ0MybWC/1QUT\n" + + "vP0BnEwEY1vSiBYJKwYBBAHaRw8BAQdAvSYTD60t8vx10dSEBACUoIfVCpeOB30D\n" + + "6nfwJtbDT0b/AGUAR05VAhAAAQIDBAUGBwgJCgsMDQ4PiNUEGBYKAH0FAmNb0ogC\n" + + "ngECmwIFFgIDAQAECwkIBwUVCgkIC18gBBkWCgAGBQJjW9KIAAoJEJQCL6VtwFtJ\n" + + "DmMBAKqsGfRFQxJXyPgugWBgEaO5lt9fMM0yUxa76cmSWe5fAQD2oLSEW1GOgIs6\n" + + "4+Z3gvtXopmeupT09HhI7ger98zDAwAKCRDDEsl9qfdqTwR6AP9Xftw8xZ7/MWhY\n" + + "Imk/xheqPy07K4qo3T1pGKUvUqjWQQEAhE3r0oTcJn+KVCwGjF6AYiLOzO/R1x5b\n" + + "SlYD3FeJ3Qo=\n" + + "=p8I9\n" + + "-----END PGP PRIVATE KEY BLOCK-----"; + + @Test + public void testMoveAllKeysToCard() throws IOException { + PGPSecretKeyRing secretKeys = PGPainless.readKeyRing().secretKeyRing(FULL_KEY); + PGPSecretKeyRing expected = PGPainless.readKeyRing().secretKeyRing(ALL_KEYS_ON_CARD); + + PGPSecretKeyRing onCard = GnuDummyKeyUtil.modify(secretKeys) + .divertPrivateKeysToCard(GnuDummyKeyUtil.KeyFilter.any(), cardSerial); + + for (PGPSecretKey key : onCard) { + assertEquals(255, key.getS2KUsage()); + S2K s2K = key.getS2K(); + assertEquals(S2K.GNU_PROTECTION_MODE_DIVERT_TO_CARD, s2K.getProtectionMode()); + } + + assertArrayEquals(expected.getEncoded(), onCard.getEncoded()); + } + + @Test + public void testMovePrimaryKeyToCard() throws IOException { + PGPSecretKeyRing secretKeys = PGPainless.readKeyRing().secretKeyRing(FULL_KEY); + PGPSecretKeyRing expected = PGPainless.readKeyRing().secretKeyRing(PRIMARY_KEY_ON_CARD); + + PGPSecretKeyRing onCard = GnuDummyKeyUtil.modify(secretKeys) + .divertPrivateKeysToCard(GnuDummyKeyUtil.KeyFilter.only(primaryKeyId), cardSerial); + + assertArrayEquals(expected.getEncoded(), onCard.getEncoded()); + } + + @Test + public void testMoveEncryptionKeyToCard() throws IOException { + PGPSecretKeyRing secretKeys = PGPainless.readKeyRing().secretKeyRing(FULL_KEY); + PGPSecretKeyRing expected = PGPainless.readKeyRing().secretKeyRing(ENCRYPTION_KEY_ON_CARD); + + PGPSecretKeyRing onCard = GnuDummyKeyUtil.modify(secretKeys) + .divertPrivateKeysToCard(GnuDummyKeyUtil.KeyFilter.only(encryptionKeyId), cardSerial); + + assertArrayEquals(expected.getEncoded(), onCard.getEncoded()); + } + + @Test + public void testMoveSigningKeyToCard() throws IOException { + PGPSecretKeyRing secretKeys = PGPainless.readKeyRing().secretKeyRing(FULL_KEY); + PGPSecretKeyRing expected = PGPainless.readKeyRing().secretKeyRing(SIGNATURE_KEY_ON_CARD); + + PGPSecretKeyRing onCard = GnuDummyKeyUtil.modify(secretKeys) + .divertPrivateKeysToCard(GnuDummyKeyUtil.KeyFilter.only(signatureKeyId), cardSerial); + + assertArrayEquals(expected.getEncoded(), onCard.getEncoded()); + } +} From a8d2319d6374111fb7287be8f27dbc8490e52dcf Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Fri, 28 Oct 2022 16:48:49 +0200 Subject: [PATCH 083/763] Add documentation to GnuDummyKeyUtil --- .../key/gnu_dummy_s2k/GnuDummyKeyUtil.java | 76 ++++++++++++++++--- 1 file changed, 66 insertions(+), 10 deletions(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/gnu_dummy_s2k/GnuDummyKeyUtil.java b/pgpainless-core/src/main/java/org/pgpainless/key/gnu_dummy_s2k/GnuDummyKeyUtil.java index a8de5aa3..f074fad2 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/key/gnu_dummy_s2k/GnuDummyKeyUtil.java +++ b/pgpainless-core/src/main/java/org/pgpainless/key/gnu_dummy_s2k/GnuDummyKeyUtil.java @@ -11,16 +11,26 @@ import org.bouncycastle.bcpg.SecretSubkeyPacket; import org.bouncycastle.openpgp.PGPSecretKey; import org.bouncycastle.openpgp.PGPSecretKeyRing; +import javax.annotation.Nonnull; import java.util.ArrayList; import java.util.Collection; import java.util.List; +/** + * This class can be used to remove private keys from secret keys. + */ public final class GnuDummyKeyUtil { private GnuDummyKeyUtil() { } + /** + * Modify the given {@link PGPSecretKeyRing}. + * + * @param secretKeys secret keys + * @return builder + */ public static Builder modify(PGPSecretKeyRing secretKeys) { return new Builder(secretKeys); } @@ -33,19 +43,50 @@ public final class GnuDummyKeyUtil { this.keys = keys; } + /** + * Remove all private keys that match the given {@link KeyFilter} from the key ring and replace them with + * GNU_DUMMY keys with S2K protection mode {@link GNUExtension#NO_PRIVATE_KEY}. + * + * @param filter filter to select keys for removal + * @return modified key ring + */ public PGPSecretKeyRing removePrivateKeys(KeyFilter filter) { - return doIt(GNUExtension.NO_PRIVATE_KEY, null, filter); + return replacePrivateKeys(GNUExtension.NO_PRIVATE_KEY, null, filter); } + /** + * Remove all private keys that match the given {@link KeyFilter} from the key ring and replace them with + * GNU_DUMMY keys with S2K protection mode {@link GNUExtension#DIVERT_TO_CARD}. + * This method will set the serial number of the card to 0x00000000000000000000000000000000. + * + * NOTE: This method does not actually move any keys to a card. + * + * @param filter filter to select keys for removal + * @return modified key ring + */ public PGPSecretKeyRing divertPrivateKeysToCard(KeyFilter filter) { return divertPrivateKeysToCard(filter, new byte[16]); } + /** + * Remove all private keys that match the given {@link KeyFilter} from the key ring and replace them with + * GNU_DUMMY keys with S2K protection mode {@link GNUExtension#DIVERT_TO_CARD}. + * This method will include the card serial number into the encoded dummy key. + * + * NOTE: This method does not actually move any keys to a card. + * + * @param filter filter to select keys for removal + * @param cardSerialNumber serial number of the card (at most 16 bytes long) + * @return modified key ring + */ public PGPSecretKeyRing divertPrivateKeysToCard(KeyFilter filter, byte[] cardSerialNumber) { - return doIt(GNUExtension.DIVERT_TO_CARD, cardSerialNumber, filter); + if (cardSerialNumber != null && cardSerialNumber.length > 16) { + throw new IllegalArgumentException("Card serial number length cannot exceed 16 bytes."); + } + return replacePrivateKeys(GNUExtension.DIVERT_TO_CARD, cardSerialNumber, filter); } - private PGPSecretKeyRing doIt(GNUExtension extension, byte[] serial, KeyFilter filter) { + private PGPSecretKeyRing replacePrivateKeys(GNUExtension extension, byte[] serial, KeyFilter filter) { byte[] encodedSerial = serial != null ? encodeSerial(serial) : null; S2K s2k = extensionToS2K(extension); @@ -71,21 +112,19 @@ public final class GnuDummyKeyUtil { } } - PGPSecretKeyRing gnuDummyKey = new PGPSecretKeyRing(secretKeyList); - return gnuDummyKey; + return new PGPSecretKeyRing(secretKeyList); } - private byte[] encodeSerial(byte[] serial) { + private byte[] encodeSerial(@Nonnull byte[] serial) { byte[] encoded = new byte[serial.length + 1]; - encoded[0] = 0x10; + encoded[0] = (byte) (serial.length & 0xff); System.arraycopy(serial, 0, encoded, 1, serial.length); return encoded; } - private S2K extensionToS2K(GNUExtension extension) { - S2K s2k = S2K.gnuDummyS2K(extension == GNUExtension.DIVERT_TO_CARD ? + private S2K extensionToS2K(@Nonnull GNUExtension extension) { + return S2K.gnuDummyS2K(extension == GNUExtension.DIVERT_TO_CARD ? S2K.GNUDummyParams.divertToCard() : S2K.GNUDummyParams.noPrivateKey()); - return s2k; } } @@ -99,15 +138,32 @@ public final class GnuDummyKeyUtil { */ boolean filter(long keyId); + /** + * Select any key. + * @return filter + */ static KeyFilter any() { return keyId -> true; } + /** + * Select only the given keyId. + * + * @param onlyKeyId only acceptable key id + * @return filter + */ static KeyFilter only(long onlyKeyId) { return keyId -> keyId == onlyKeyId; } + /** + * Select all keyIds which are contained in the given set of ids. + * + * @param ids set of acceptable keyIds + * @return filter + */ static KeyFilter selected(Collection ids) { + // noinspection Convert2MethodRef return keyId -> ids.contains(keyId); } } From 033beaa8f2ae1c16e552cd8d2004fff27307a86b Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Fri, 28 Oct 2022 17:05:56 +0200 Subject: [PATCH 084/763] Use S2K usage SHA1 in GnuDummyKeyUtil --- .../key/gnu_dummy_s2k/GNUExtension.java | 7 ++++++ .../key/gnu_dummy_s2k/GnuDummyKeyUtil.java | 6 ++--- .../gnu_dummy_s2k/GnuDummyKeyUtilTest.java | 23 ++++++++++--------- 3 files changed, 22 insertions(+), 14 deletions(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/gnu_dummy_s2k/GNUExtension.java b/pgpainless-core/src/main/java/org/pgpainless/key/gnu_dummy_s2k/GNUExtension.java index ff42ef6e..e829bd7b 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/key/gnu_dummy_s2k/GNUExtension.java +++ b/pgpainless-core/src/main/java/org/pgpainless/key/gnu_dummy_s2k/GNUExtension.java @@ -8,7 +8,14 @@ import org.bouncycastle.bcpg.S2K; public enum GNUExtension { + /** + * Do not store the secret part at all. + */ NO_PRIVATE_KEY(S2K.GNU_PROTECTION_MODE_NO_PRIVATE_KEY), + + /** + * A stub to access smartcards. + */ DIVERT_TO_CARD(S2K.GNU_PROTECTION_MODE_DIVERT_TO_CARD), ; diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/gnu_dummy_s2k/GnuDummyKeyUtil.java b/pgpainless-core/src/main/java/org/pgpainless/key/gnu_dummy_s2k/GnuDummyKeyUtil.java index f074fad2..7817a676 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/key/gnu_dummy_s2k/GnuDummyKeyUtil.java +++ b/pgpainless-core/src/main/java/org/pgpainless/key/gnu_dummy_s2k/GnuDummyKeyUtil.java @@ -74,7 +74,7 @@ public final class GnuDummyKeyUtil { * This method will include the card serial number into the encoded dummy key. * * NOTE: This method does not actually move any keys to a card. - * + * * @param filter filter to select keys for removal * @param cardSerialNumber serial number of the card (at most 16 bytes long) * @return modified key ring @@ -101,12 +101,12 @@ public final class GnuDummyKeyUtil { PublicKeyPacket publicKeyPacket = secretKey.getPublicKey().getPublicKeyPacket(); if (secretKey.isMasterKey()) { SecretKeyPacket keyPacket = new SecretKeyPacket(publicKeyPacket, - 0, 255, s2k, null, encodedSerial); + 0, SecretKeyPacket.USAGE_SHA1, s2k, null, encodedSerial); PGPSecretKey onCard = new PGPSecretKey(keyPacket, secretKey.getPublicKey()); secretKeyList.add(onCard); } else { SecretSubkeyPacket keyPacket = new SecretSubkeyPacket(publicKeyPacket, - 0, 255, s2k, null, encodedSerial); + 0, SecretKeyPacket.USAGE_SHA1, s2k, null, encodedSerial); PGPSecretKey onCard = new PGPSecretKey(keyPacket, secretKey.getPublicKey()); secretKeyList.add(onCard); } diff --git a/pgpainless-core/src/test/java/org/pgpainless/key/gnu_dummy_s2k/GnuDummyKeyUtilTest.java b/pgpainless-core/src/test/java/org/pgpainless/key/gnu_dummy_s2k/GnuDummyKeyUtilTest.java index 0b19c551..99966903 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/key/gnu_dummy_s2k/GnuDummyKeyUtilTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/key/gnu_dummy_s2k/GnuDummyKeyUtilTest.java @@ -5,6 +5,7 @@ package org.pgpainless.key.gnu_dummy_s2k; import org.bouncycastle.bcpg.S2K; +import org.bouncycastle.bcpg.SecretKeyPacket; import org.bouncycastle.openpgp.PGPSecretKey; import org.bouncycastle.openpgp.PGPSecretKeyRing; import org.junit.jupiter.api.Test; @@ -54,22 +55,22 @@ public class GnuDummyKeyUtilTest { "Comment: Hardy Hardware \n" + "\n" + "lEwEY1vSiBYJKwYBBAHaRw8BAQdAQ58lZn/HOtg+1b1KS18odyQ6M4LaDdbJAyRf\n" + - "eBwCeTT/AGUAR05VAhAAAQIDBAUGBwgJCgsMDQ4PtCBIYXJkeSBIYXJkd2FyZSA8\n" + + "eBwCeTT+AGUAR05VAhAAAQIDBAUGBwgJCgsMDQ4PtCBIYXJkeSBIYXJkd2FyZSA8\n" + "aGFyZHlAaGFyZC53YXJlPoiPBBMWCgBBBQJjW9KICRDDEsl9qfdqTxYhBAH9q2zg\n" + "SlB4ef5KGMMSyX2p92pPAp4BApsBBRYCAwEABAsJCAcFFQoJCAsCmQEAAPk2AP92\n" + "2T5TQ7hukFlpxX3ThMhieJnECGY5Eqt5U0/vEY1XdgD/eE1Ml9qqx6QGcaNKe8de\n" + "Me3EhTant6mS9tqMHp2/3gmcUQRjW9KIEgorBgEEAZdVAQUBAQdAVXBLNvNmFh9K\n" + - "X6iLmdNJM28Zc9PGnzEoAD9+T4p0lDwDAQgH/wBlAEdOVQIQAAECAwQFBgcICQoL\n" + + "X6iLmdNJM28Zc9PGnzEoAD9+T4p0lDwDAQgH/gBlAEdOVQIQAAECAwQFBgcICQoL\n" + "DA0OD4h1BBgWCgAdBQJjW9KIAp4BApsMBRYCAwEABAsJCAcFFQoJCAsACgkQwxLJ\n" + "fan3ak/JyQD9GBj0vjtYZAf5Fi0eEKdiAgs0yZrQPkMs6eL+83te770A/jG0DeJy\n" + "+88fOfWTj+mixO98PZPnQ0MybWC/1QUTvP0BnEwEY1vSiBYJKwYBBAHaRw8BAQdA\n" + - "vSYTD60t8vx10dSEBACUoIfVCpeOB30D6nfwJtbDT0b/AGUAR05VAhAAAQIDBAUG\n" + + "vSYTD60t8vx10dSEBACUoIfVCpeOB30D6nfwJtbDT0b+AGUAR05VAhAAAQIDBAUG\n" + "BwgJCgsMDQ4PiNUEGBYKAH0FAmNb0ogCngECmwIFFgIDAQAECwkIBwUVCgkIC18g\n" + "BBkWCgAGBQJjW9KIAAoJEJQCL6VtwFtJDmMBAKqsGfRFQxJXyPgugWBgEaO5lt9f\n" + "MM0yUxa76cmSWe5fAQD2oLSEW1GOgIs64+Z3gvtXopmeupT09HhI7ger98zDAwAK\n" + "CRDDEsl9qfdqTwR6AP9Xftw8xZ7/MWhYImk/xheqPy07K4qo3T1pGKUvUqjWQQEA\n" + "hE3r0oTcJn+KVCwGjF6AYiLOzO/R1x5bSlYD3FeJ3Qo=\n" + - "=wsFa\n" + + "=rYoa\n" + "-----END PGP PRIVATE KEY BLOCK-----"; public static final String PRIMARY_KEY_ON_CARD = "-----BEGIN PGP PRIVATE KEY BLOCK-----\n" + @@ -78,7 +79,7 @@ public class GnuDummyKeyUtilTest { "Comment: Hardy Hardware \n" + "\n" + "lEwEY1vSiBYJKwYBBAHaRw8BAQdAQ58lZn/HOtg+1b1KS18odyQ6M4LaDdbJAyRf\n" + - "eBwCeTT/AGUAR05VAhAAAQIDBAUGBwgJCgsMDQ4PtCBIYXJkeSBIYXJkd2FyZSA8\n" + + "eBwCeTT+AGUAR05VAhAAAQIDBAUGBwgJCgsMDQ4PtCBIYXJkeSBIYXJkd2FyZSA8\n" + "aGFyZHlAaGFyZC53YXJlPoiPBBMWCgBBBQJjW9KICRDDEsl9qfdqTxYhBAH9q2zg\n" + "SlB4ef5KGMMSyX2p92pPAp4BApsBBRYCAwEABAsJCAcFFQoJCAsCmQEAAPk2AP92\n" + "2T5TQ7hukFlpxX3ThMhieJnECGY5Eqt5U0/vEY1XdgD/eE1Ml9qqx6QGcaNKe8de\n" + @@ -94,7 +95,7 @@ public class GnuDummyKeyUtilTest { "4+Z3gvtXopmeupT09HhI7ger98zDAwAKCRDDEsl9qfdqTwR6AP9Xftw8xZ7/MWhY\n" + "Imk/xheqPy07K4qo3T1pGKUvUqjWQQEAhE3r0oTcJn+KVCwGjF6AYiLOzO/R1x5b\n" + "SlYD3FeJ3Qo=\n" + - "=s+B1\n" + + "=zQLi\n" + "-----END PGP PRIVATE KEY BLOCK-----"; public static final String ENCRYPTION_KEY_ON_CARD = "-----BEGIN PGP PRIVATE KEY BLOCK-----\n" + @@ -108,7 +109,7 @@ public class GnuDummyKeyUtilTest { "qfdqTxYhBAH9q2zgSlB4ef5KGMMSyX2p92pPAp4BApsBBRYCAwEABAsJCAcFFQoJ\n" + "CAsCmQEAAPk2AP922T5TQ7hukFlpxX3ThMhieJnECGY5Eqt5U0/vEY1XdgD/eE1M\n" + "l9qqx6QGcaNKe8deMe3EhTant6mS9tqMHp2/3gmcUQRjW9KIEgorBgEEAZdVAQUB\n" + - "AQdAVXBLNvNmFh9KX6iLmdNJM28Zc9PGnzEoAD9+T4p0lDwDAQgH/wBlAEdOVQIQ\n" + + "AQdAVXBLNvNmFh9KX6iLmdNJM28Zc9PGnzEoAD9+T4p0lDwDAQgH/gBlAEdOVQIQ\n" + "AAECAwQFBgcICQoLDA0OD4h1BBgWCgAdBQJjW9KIAp4BApsMBRYCAwEABAsJCAcF\n" + "FQoJCAsACgkQwxLJfan3ak/JyQD9GBj0vjtYZAf5Fi0eEKdiAgs0yZrQPkMs6eL+\n" + "83te770A/jG0DeJy+88fOfWTj+mixO98PZPnQ0MybWC/1QUTvP0BnFgEY1vSiBYJ\n" + @@ -119,7 +120,7 @@ public class GnuDummyKeyUtilTest { "4+Z3gvtXopmeupT09HhI7ger98zDAwAKCRDDEsl9qfdqTwR6AP9Xftw8xZ7/MWhY\n" + "Imk/xheqPy07K4qo3T1pGKUvUqjWQQEAhE3r0oTcJn+KVCwGjF6AYiLOzO/R1x5b\n" + "SlYD3FeJ3Qo=\n" + - "=TPAl\n" + + "=7OZu\n" + "-----END PGP PRIVATE KEY BLOCK-----"; public static final String SIGNATURE_KEY_ON_CARD = "-----BEGIN PGP PRIVATE KEY BLOCK-----\n" + @@ -138,13 +139,13 @@ public class GnuDummyKeyUtilTest { "BRYCAwEABAsJCAcFFQoJCAsACgkQwxLJfan3ak/JyQD9GBj0vjtYZAf5Fi0eEKdi\n" + "Ags0yZrQPkMs6eL+83te770A/jG0DeJy+88fOfWTj+mixO98PZPnQ0MybWC/1QUT\n" + "vP0BnEwEY1vSiBYJKwYBBAHaRw8BAQdAvSYTD60t8vx10dSEBACUoIfVCpeOB30D\n" + - "6nfwJtbDT0b/AGUAR05VAhAAAQIDBAUGBwgJCgsMDQ4PiNUEGBYKAH0FAmNb0ogC\n" + + "6nfwJtbDT0b+AGUAR05VAhAAAQIDBAUGBwgJCgsMDQ4PiNUEGBYKAH0FAmNb0ogC\n" + "ngECmwIFFgIDAQAECwkIBwUVCgkIC18gBBkWCgAGBQJjW9KIAAoJEJQCL6VtwFtJ\n" + "DmMBAKqsGfRFQxJXyPgugWBgEaO5lt9fMM0yUxa76cmSWe5fAQD2oLSEW1GOgIs6\n" + "4+Z3gvtXopmeupT09HhI7ger98zDAwAKCRDDEsl9qfdqTwR6AP9Xftw8xZ7/MWhY\n" + "Imk/xheqPy07K4qo3T1pGKUvUqjWQQEAhE3r0oTcJn+KVCwGjF6AYiLOzO/R1x5b\n" + "SlYD3FeJ3Qo=\n" + - "=p8I9\n" + + "=GpEw\n" + "-----END PGP PRIVATE KEY BLOCK-----"; @Test @@ -156,7 +157,7 @@ public class GnuDummyKeyUtilTest { .divertPrivateKeysToCard(GnuDummyKeyUtil.KeyFilter.any(), cardSerial); for (PGPSecretKey key : onCard) { - assertEquals(255, key.getS2KUsage()); + assertEquals(SecretKeyPacket.USAGE_SHA1, key.getS2KUsage()); S2K s2K = key.getS2K(); assertEquals(S2K.GNU_PROTECTION_MODE_DIVERT_TO_CARD, s2K.getProtectionMode()); } From 3af6ab1b85b7f8cd554fcb6e848e205b3038d3a2 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Sat, 29 Oct 2022 14:09:41 +0200 Subject: [PATCH 085/763] Rename GnuPGDummyExtension + GnuPGDummyKeyUtil --- ...xtension.java => GnuPGDummyExtension.java} | 4 +-- ...mmyKeyUtil.java => GnuPGDummyKeyUtil.java} | 31 ++++++++++++------- .../HardwareSecurityTest.java | 10 +++--- ...ilTest.java => GnuPGDummyKeyUtilTest.java} | 18 +++++------ 4 files changed, 36 insertions(+), 27 deletions(-) rename pgpainless-core/src/main/java/org/pgpainless/key/gnu_dummy_s2k/{GNUExtension.java => GnuPGDummyExtension.java} (88%) rename pgpainless-core/src/main/java/org/pgpainless/key/gnu_dummy_s2k/{GnuDummyKeyUtil.java => GnuPGDummyKeyUtil.java} (81%) rename pgpainless-core/src/test/java/org/pgpainless/key/gnu_dummy_s2k/{GnuDummyKeyUtilTest.java => GnuPGDummyKeyUtilTest.java} (93%) diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/gnu_dummy_s2k/GNUExtension.java b/pgpainless-core/src/main/java/org/pgpainless/key/gnu_dummy_s2k/GnuPGDummyExtension.java similarity index 88% rename from pgpainless-core/src/main/java/org/pgpainless/key/gnu_dummy_s2k/GNUExtension.java rename to pgpainless-core/src/main/java/org/pgpainless/key/gnu_dummy_s2k/GnuPGDummyExtension.java index e829bd7b..9f75bf7e 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/key/gnu_dummy_s2k/GNUExtension.java +++ b/pgpainless-core/src/main/java/org/pgpainless/key/gnu_dummy_s2k/GnuPGDummyExtension.java @@ -6,7 +6,7 @@ package org.pgpainless.key.gnu_dummy_s2k; import org.bouncycastle.bcpg.S2K; -public enum GNUExtension { +public enum GnuPGDummyExtension { /** * Do not store the secret part at all. @@ -21,7 +21,7 @@ public enum GNUExtension { private final int id; - GNUExtension(int id) { + GnuPGDummyExtension(int id) { this.id = id; } diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/gnu_dummy_s2k/GnuDummyKeyUtil.java b/pgpainless-core/src/main/java/org/pgpainless/key/gnu_dummy_s2k/GnuPGDummyKeyUtil.java similarity index 81% rename from pgpainless-core/src/main/java/org/pgpainless/key/gnu_dummy_s2k/GnuDummyKeyUtil.java rename to pgpainless-core/src/main/java/org/pgpainless/key/gnu_dummy_s2k/GnuPGDummyKeyUtil.java index 7817a676..3a913894 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/key/gnu_dummy_s2k/GnuDummyKeyUtil.java +++ b/pgpainless-core/src/main/java/org/pgpainless/key/gnu_dummy_s2k/GnuPGDummyKeyUtil.java @@ -17,11 +17,15 @@ import java.util.Collection; import java.util.List; /** - * This class can be used to remove private keys from secret keys. + * This class can be used to remove private keys from secret software-keys by replacing them with + * stub secret keys in the style of GnuPGs proprietary extensions. + * + * @see + * GnuPGs doc/DETAILS - GNU extensions to the S2K algorithm */ -public final class GnuDummyKeyUtil { +public final class GnuPGDummyKeyUtil { - private GnuDummyKeyUtil() { + private GnuPGDummyKeyUtil() { } @@ -45,18 +49,18 @@ public final class GnuDummyKeyUtil { /** * Remove all private keys that match the given {@link KeyFilter} from the key ring and replace them with - * GNU_DUMMY keys with S2K protection mode {@link GNUExtension#NO_PRIVATE_KEY}. + * GNU_DUMMY keys with S2K protection mode {@link GnuPGDummyExtension#NO_PRIVATE_KEY}. * * @param filter filter to select keys for removal * @return modified key ring */ public PGPSecretKeyRing removePrivateKeys(KeyFilter filter) { - return replacePrivateKeys(GNUExtension.NO_PRIVATE_KEY, null, filter); + return replacePrivateKeys(GnuPGDummyExtension.NO_PRIVATE_KEY, null, filter); } /** * Remove all private keys that match the given {@link KeyFilter} from the key ring and replace them with - * GNU_DUMMY keys with S2K protection mode {@link GNUExtension#DIVERT_TO_CARD}. + * GNU_DUMMY keys with S2K protection mode {@link GnuPGDummyExtension#DIVERT_TO_CARD}. * This method will set the serial number of the card to 0x00000000000000000000000000000000. * * NOTE: This method does not actually move any keys to a card. @@ -70,7 +74,7 @@ public final class GnuDummyKeyUtil { /** * Remove all private keys that match the given {@link KeyFilter} from the key ring and replace them with - * GNU_DUMMY keys with S2K protection mode {@link GNUExtension#DIVERT_TO_CARD}. + * GNU_DUMMY keys with S2K protection mode {@link GnuPGDummyExtension#DIVERT_TO_CARD}. * This method will include the card serial number into the encoded dummy key. * * NOTE: This method does not actually move any keys to a card. @@ -83,10 +87,10 @@ public final class GnuDummyKeyUtil { if (cardSerialNumber != null && cardSerialNumber.length > 16) { throw new IllegalArgumentException("Card serial number length cannot exceed 16 bytes."); } - return replacePrivateKeys(GNUExtension.DIVERT_TO_CARD, cardSerialNumber, filter); + return replacePrivateKeys(GnuPGDummyExtension.DIVERT_TO_CARD, cardSerialNumber, filter); } - private PGPSecretKeyRing replacePrivateKeys(GNUExtension extension, byte[] serial, KeyFilter filter) { + private PGPSecretKeyRing replacePrivateKeys(GnuPGDummyExtension extension, byte[] serial, KeyFilter filter) { byte[] encodedSerial = serial != null ? encodeSerial(serial) : null; S2K s2k = extensionToS2K(extension); @@ -122,12 +126,16 @@ public final class GnuDummyKeyUtil { return encoded; } - private S2K extensionToS2K(@Nonnull GNUExtension extension) { - return S2K.gnuDummyS2K(extension == GNUExtension.DIVERT_TO_CARD ? + private S2K extensionToS2K(@Nonnull GnuPGDummyExtension extension) { + return S2K.gnuDummyS2K(extension == GnuPGDummyExtension.DIVERT_TO_CARD ? S2K.GNUDummyParams.divertToCard() : S2K.GNUDummyParams.noPrivateKey()); } } + /** + * Filter for selecting keys. + */ + @FunctionalInterface public interface KeyFilter { /** @@ -140,6 +148,7 @@ public final class GnuDummyKeyUtil { /** * Select any key. + * * @return filter */ static KeyFilter any() { diff --git a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/HardwareSecurityTest.java b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/HardwareSecurityTest.java index f1606cec..a2160edf 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/HardwareSecurityTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/HardwareSecurityTest.java @@ -17,7 +17,7 @@ import org.bouncycastle.openpgp.PGPSecretKeyRing; import org.junit.jupiter.api.Test; import org.pgpainless.PGPainless; import org.pgpainless.key.SubkeyIdentifier; -import org.pgpainless.key.gnu_dummy_s2k.GnuDummyKeyUtil; +import org.pgpainless.key.gnu_dummy_s2k.GnuPGDummyKeyUtil; import org.pgpainless.key.util.KeyIdUtil; public class HardwareSecurityTest { @@ -53,8 +53,8 @@ public class HardwareSecurityTest { assertTrue(HardwareSecurity.getIdsOfHardwareBackedKeys(secretKeys).isEmpty()); long encryptionKeyId = KeyIdUtil.fromLongKeyId("0AAD8F5891262F50"); - PGPSecretKeyRing withHardwareBackedEncryptionKey = GnuDummyKeyUtil.modify(secretKeys) - .divertPrivateKeysToCard(GnuDummyKeyUtil.KeyFilter.only(encryptionKeyId)); + PGPSecretKeyRing withHardwareBackedEncryptionKey = GnuPGDummyKeyUtil.modify(secretKeys) + .divertPrivateKeysToCard(GnuPGDummyKeyUtil.KeyFilter.only(encryptionKeyId)); Set hardwareBackedKeys = HardwareSecurity .getIdsOfHardwareBackedKeys(withHardwareBackedEncryptionKey); @@ -67,8 +67,8 @@ public class HardwareSecurityTest { PGPSecretKeyRing secretKeys = PGPainless.readKeyRing().secretKeyRing(KEY); assertTrue(HardwareSecurity.getIdsOfHardwareBackedKeys(secretKeys).isEmpty()); - PGPSecretKeyRing withHardwareBackedEncryptionKey = GnuDummyKeyUtil.modify(secretKeys) - .divertPrivateKeysToCard(GnuDummyKeyUtil.KeyFilter.any()); + PGPSecretKeyRing withHardwareBackedEncryptionKey = GnuPGDummyKeyUtil.modify(secretKeys) + .divertPrivateKeysToCard(GnuPGDummyKeyUtil.KeyFilter.any()); Set expected = new HashSet<>(); for (PGPSecretKey key : secretKeys) { expected.add(new SubkeyIdentifier(secretKeys, key.getKeyID())); diff --git a/pgpainless-core/src/test/java/org/pgpainless/key/gnu_dummy_s2k/GnuDummyKeyUtilTest.java b/pgpainless-core/src/test/java/org/pgpainless/key/gnu_dummy_s2k/GnuPGDummyKeyUtilTest.java similarity index 93% rename from pgpainless-core/src/test/java/org/pgpainless/key/gnu_dummy_s2k/GnuDummyKeyUtilTest.java rename to pgpainless-core/src/test/java/org/pgpainless/key/gnu_dummy_s2k/GnuPGDummyKeyUtilTest.java index 99966903..1fc3b892 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/key/gnu_dummy_s2k/GnuDummyKeyUtilTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/key/gnu_dummy_s2k/GnuPGDummyKeyUtilTest.java @@ -17,7 +17,7 @@ import java.io.IOException; import static org.junit.jupiter.api.Assertions.assertArrayEquals; import static org.junit.jupiter.api.Assertions.assertEquals; -public class GnuDummyKeyUtilTest { +public class GnuPGDummyKeyUtilTest { // normal, non-hw-backed key private static final String FULL_KEY = "-----BEGIN PGP PRIVATE KEY BLOCK-----\n" + "Version: PGPainless\n" + @@ -153,8 +153,8 @@ public class GnuDummyKeyUtilTest { PGPSecretKeyRing secretKeys = PGPainless.readKeyRing().secretKeyRing(FULL_KEY); PGPSecretKeyRing expected = PGPainless.readKeyRing().secretKeyRing(ALL_KEYS_ON_CARD); - PGPSecretKeyRing onCard = GnuDummyKeyUtil.modify(secretKeys) - .divertPrivateKeysToCard(GnuDummyKeyUtil.KeyFilter.any(), cardSerial); + PGPSecretKeyRing onCard = GnuPGDummyKeyUtil.modify(secretKeys) + .divertPrivateKeysToCard(GnuPGDummyKeyUtil.KeyFilter.any(), cardSerial); for (PGPSecretKey key : onCard) { assertEquals(SecretKeyPacket.USAGE_SHA1, key.getS2KUsage()); @@ -170,8 +170,8 @@ public class GnuDummyKeyUtilTest { PGPSecretKeyRing secretKeys = PGPainless.readKeyRing().secretKeyRing(FULL_KEY); PGPSecretKeyRing expected = PGPainless.readKeyRing().secretKeyRing(PRIMARY_KEY_ON_CARD); - PGPSecretKeyRing onCard = GnuDummyKeyUtil.modify(secretKeys) - .divertPrivateKeysToCard(GnuDummyKeyUtil.KeyFilter.only(primaryKeyId), cardSerial); + PGPSecretKeyRing onCard = GnuPGDummyKeyUtil.modify(secretKeys) + .divertPrivateKeysToCard(GnuPGDummyKeyUtil.KeyFilter.only(primaryKeyId), cardSerial); assertArrayEquals(expected.getEncoded(), onCard.getEncoded()); } @@ -181,8 +181,8 @@ public class GnuDummyKeyUtilTest { PGPSecretKeyRing secretKeys = PGPainless.readKeyRing().secretKeyRing(FULL_KEY); PGPSecretKeyRing expected = PGPainless.readKeyRing().secretKeyRing(ENCRYPTION_KEY_ON_CARD); - PGPSecretKeyRing onCard = GnuDummyKeyUtil.modify(secretKeys) - .divertPrivateKeysToCard(GnuDummyKeyUtil.KeyFilter.only(encryptionKeyId), cardSerial); + PGPSecretKeyRing onCard = GnuPGDummyKeyUtil.modify(secretKeys) + .divertPrivateKeysToCard(GnuPGDummyKeyUtil.KeyFilter.only(encryptionKeyId), cardSerial); assertArrayEquals(expected.getEncoded(), onCard.getEncoded()); } @@ -192,8 +192,8 @@ public class GnuDummyKeyUtilTest { PGPSecretKeyRing secretKeys = PGPainless.readKeyRing().secretKeyRing(FULL_KEY); PGPSecretKeyRing expected = PGPainless.readKeyRing().secretKeyRing(SIGNATURE_KEY_ON_CARD); - PGPSecretKeyRing onCard = GnuDummyKeyUtil.modify(secretKeys) - .divertPrivateKeysToCard(GnuDummyKeyUtil.KeyFilter.only(signatureKeyId), cardSerial); + PGPSecretKeyRing onCard = GnuPGDummyKeyUtil.modify(secretKeys) + .divertPrivateKeysToCard(GnuPGDummyKeyUtil.KeyFilter.only(signatureKeyId), cardSerial); assertArrayEquals(expected.getEncoded(), onCard.getEncoded()); } From df4fc94ce715ea63278d2379df0e75c07f75c627 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Sat, 29 Oct 2022 14:51:39 +0200 Subject: [PATCH 086/763] Add test for decryption with removed private key --- .../HardwareSecurity.java | 31 ------- .../key/gnu_dummy_s2k/GnuPGDummyKeyUtil.java | 30 +++++++ .../HardwareSecurityTest.java | 82 ------------------- ...DecryptWithUnavailableGnuDummyKeyTest.java | 52 ++++++++++++ .../gnu_dummy_s2k/GnuPGDummyKeyUtilTest.java | 77 +++++++++++++++++ 5 files changed, 159 insertions(+), 113 deletions(-) delete mode 100644 pgpainless-core/src/test/java/org/pgpainless/decryption_verification/HardwareSecurityTest.java create mode 100644 pgpainless-core/src/test/java/org/pgpainless/decryption_verification/TryDecryptWithUnavailableGnuDummyKeyTest.java diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/HardwareSecurity.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/HardwareSecurity.java index 6d9719dd..daf902d4 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/HardwareSecurity.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/HardwareSecurity.java @@ -4,18 +4,12 @@ package org.pgpainless.decryption_verification; -import org.bouncycastle.bcpg.S2K; import org.bouncycastle.openpgp.PGPException; -import org.bouncycastle.openpgp.PGPSecretKey; -import org.bouncycastle.openpgp.PGPSecretKeyRing; import org.bouncycastle.openpgp.operator.PGPDataDecryptor; import org.bouncycastle.openpgp.operator.PublicKeyDataDecryptorFactory; import org.bouncycastle.openpgp.operator.bc.BcPublicKeyDataDecryptorFactory; import org.pgpainless.key.SubkeyIdentifier; -import java.util.HashSet; -import java.util.Set; - /** * Enable integration of hardware-backed OpenPGP keys. */ @@ -41,31 +35,6 @@ public class HardwareSecurity { } - /** - * Return the key-ids of all keys which appear to be stored on a hardware token / smartcard. - * - * @param secretKeys secret keys - * @return set of keys with S2K type DIVERT_TO_CARD or GNU_DUMMY_S2K - */ - public static Set getIdsOfHardwareBackedKeys(PGPSecretKeyRing secretKeys) { - Set hardwareBackedKeys = new HashSet<>(); - for (PGPSecretKey secretKey : secretKeys) { - S2K s2K = secretKey.getS2K(); - if (s2K == null) { - continue; - } - - int type = s2K.getType(); - int mode = s2K.getProtectionMode(); - // TODO: Is GNU_DUMMY_S2K appropriate? - if (type == S2K.GNU_DUMMY_S2K && mode == S2K.GNU_PROTECTION_MODE_DIVERT_TO_CARD) { - SubkeyIdentifier hardwareBackedKey = new SubkeyIdentifier(secretKeys, secretKey.getKeyID()); - hardwareBackedKeys.add(hardwareBackedKey); - } - } - return hardwareBackedKeys; - } - /** * Implementation of {@link PublicKeyDataDecryptorFactory} which delegates decryption of encrypted session keys * to a {@link DecryptionCallback}. diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/gnu_dummy_s2k/GnuPGDummyKeyUtil.java b/pgpainless-core/src/main/java/org/pgpainless/key/gnu_dummy_s2k/GnuPGDummyKeyUtil.java index 3a913894..64c7ed26 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/key/gnu_dummy_s2k/GnuPGDummyKeyUtil.java +++ b/pgpainless-core/src/main/java/org/pgpainless/key/gnu_dummy_s2k/GnuPGDummyKeyUtil.java @@ -10,11 +10,14 @@ import org.bouncycastle.bcpg.SecretKeyPacket; import org.bouncycastle.bcpg.SecretSubkeyPacket; import org.bouncycastle.openpgp.PGPSecretKey; import org.bouncycastle.openpgp.PGPSecretKeyRing; +import org.pgpainless.key.SubkeyIdentifier; import javax.annotation.Nonnull; import java.util.ArrayList; import java.util.Collection; +import java.util.HashSet; import java.util.List; +import java.util.Set; /** * This class can be used to remove private keys from secret software-keys by replacing them with @@ -29,6 +32,33 @@ public final class GnuPGDummyKeyUtil { } + /** + * Return the key-ids of all keys which appear to be stored on a hardware token / smartcard by GnuPG. + * Note, that this functionality is based on GnuPGs proprietary S2K extensions, which are not strictly required + * for dealing with hardware-backed keys. + * + * @param secretKeys secret keys + * @return set of keys with S2K type GNU_DUMMY_S2K and protection mode DIVERT_TO_CARD + */ + public static Set getIdsOfKeysWithGnuPGS2KDivertedToCard(PGPSecretKeyRing secretKeys) { + Set hardwareBackedKeys = new HashSet<>(); + for (PGPSecretKey secretKey : secretKeys) { + S2K s2K = secretKey.getS2K(); + if (s2K == null) { + continue; + } + + int type = s2K.getType(); + int mode = s2K.getProtectionMode(); + // TODO: Is GNU_DUMMY_S2K appropriate? + if (type == S2K.GNU_DUMMY_S2K && mode == S2K.GNU_PROTECTION_MODE_DIVERT_TO_CARD) { + SubkeyIdentifier hardwareBackedKey = new SubkeyIdentifier(secretKeys, secretKey.getKeyID()); + hardwareBackedKeys.add(hardwareBackedKey); + } + } + return hardwareBackedKeys; + } + /** * Modify the given {@link PGPSecretKeyRing}. * diff --git a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/HardwareSecurityTest.java b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/HardwareSecurityTest.java deleted file mode 100644 index a2160edf..00000000 --- a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/HardwareSecurityTest.java +++ /dev/null @@ -1,82 +0,0 @@ -// SPDX-FileCopyrightText: 2022 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.decryption_verification; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertTrue; - -import java.io.IOException; -import java.util.Collections; -import java.util.HashSet; -import java.util.Set; - -import org.bouncycastle.openpgp.PGPSecretKey; -import org.bouncycastle.openpgp.PGPSecretKeyRing; -import org.junit.jupiter.api.Test; -import org.pgpainless.PGPainless; -import org.pgpainless.key.SubkeyIdentifier; -import org.pgpainless.key.gnu_dummy_s2k.GnuPGDummyKeyUtil; -import org.pgpainless.key.util.KeyIdUtil; - -public class HardwareSecurityTest { - - private static final String KEY = "-----BEGIN PGP PRIVATE KEY BLOCK-----\n" + - "Version: PGPainless\n" + - "Comment: DE2E 9AB2 6650 8191 53E7 D599 C176 507F 2B5D 43B3\n" + - "Comment: Alice \n" + - "\n" + - "lFgEY1vjgRYJKwYBBAHaRw8BAQdAXjLoPTOIOdvlFT2Nt3rcvLTVx5ujPBGghZ5S\n" + - "D5tEnyoAAP0fAUJTiPrxZYdzs6MP0KFo+Nmr/wb1PJHTkzmYpt4wkRKBtBxBbGlj\n" + - "ZSA8YWxpY2VAcGdwYWlubGVzcy5vcmc+iI8EExYKAEEFAmNb44EJEMF2UH8rXUOz\n" + - "FiEE3i6asmZQgZFT59WZwXZQfytdQ7MCngECmwEFFgIDAQAECwkIBwUVCgkICwKZ\n" + - "AQAAHLYA/AgW+YrpU+UqrwX2dhY6RAfgHTTMU89RHjaTHJx8pLrBAP4gthGof00a\n" + - "XEjwTWteDOO049SIp2AUfj9deJqtrQcHD5xdBGNb44ESCisGAQQBl1UBBQEBB0DN\n" + - "vUT3awa3YLmwf41LRpPrm7B87AOHfYIP8S9QJ4GDJgMBCAcAAP9bwlSaF+lti8JY\n" + - "qKFO3qt3ZYQMu1l/LRBle89ZB4zD+BDOiHUEGBYKAB0FAmNb44ECngECmwwFFgID\n" + - "AQAECwkIBwUVCgkICwAKCRDBdlB/K11Ds/TsAP9kvpUrCWnrWGq+a9n1CqEfCMX5\n" + - "cT+qzrwNf+J0L22KowD+M9SVO0qssiAqutLE9h9dGYLbEiFvsHzK3WSnjKYbIgac\n" + - "WARjW+OBFgkrBgEEAdpHDwEBB0BCPh8M5TnXSmG6Ygwp4j5RR4u3hmxl8CYjX4h/\n" + - "XtvvNwAA/RP04coSrLHVI6vUfbJk4MhWYeyhJBRYY0vGp7yq+wVtEpKI1QQYFgoA\n" + - "fQUCY1vjgQKeAQKbAgUWAgMBAAQLCQgHBRUKCQgLXyAEGRYKAAYFAmNb44EACgkQ\n" + - "mlozJSF7rXQW+AD/TA3YBxTd+YbBSwfgqzWNbfT9BBcFrdn3uPCsbvfmqXoA/3oj\n" + - "oupkgoaXesrGxn2k9hW9/GBXSvNcgY2txZ6/oYoIAAoJEMF2UH8rXUOziZ4A/0Xl\n" + - "xSZJWmkRpBh5AO8Cnqosz6j947IYAxS16ay+sIOHAP9aN9CUNJIIdHnHdFHO4GZz\n" + - "ejjknn4wt8NVJP97JxlnBQ==\n" + - "=qSQb\n" + - "-----END PGP PRIVATE KEY BLOCK-----"; - - @Test - public void testGetSingleIdOfHardwareBackedKey() throws IOException { - PGPSecretKeyRing secretKeys = PGPainless.readKeyRing().secretKeyRing(KEY); - assertTrue(HardwareSecurity.getIdsOfHardwareBackedKeys(secretKeys).isEmpty()); - long encryptionKeyId = KeyIdUtil.fromLongKeyId("0AAD8F5891262F50"); - - PGPSecretKeyRing withHardwareBackedEncryptionKey = GnuPGDummyKeyUtil.modify(secretKeys) - .divertPrivateKeysToCard(GnuPGDummyKeyUtil.KeyFilter.only(encryptionKeyId)); - - Set hardwareBackedKeys = HardwareSecurity - .getIdsOfHardwareBackedKeys(withHardwareBackedEncryptionKey); - assertEquals(Collections.singleton(new SubkeyIdentifier(secretKeys, encryptionKeyId)), hardwareBackedKeys); - } - - - @Test - public void testGetIdsOfFullyHardwareBackedKey() throws IOException { - PGPSecretKeyRing secretKeys = PGPainless.readKeyRing().secretKeyRing(KEY); - assertTrue(HardwareSecurity.getIdsOfHardwareBackedKeys(secretKeys).isEmpty()); - - PGPSecretKeyRing withHardwareBackedEncryptionKey = GnuPGDummyKeyUtil.modify(secretKeys) - .divertPrivateKeysToCard(GnuPGDummyKeyUtil.KeyFilter.any()); - Set expected = new HashSet<>(); - for (PGPSecretKey key : secretKeys) { - expected.add(new SubkeyIdentifier(secretKeys, key.getKeyID())); - } - - Set hardwareBackedKeys = HardwareSecurity - .getIdsOfHardwareBackedKeys(withHardwareBackedEncryptionKey); - - assertEquals(expected, hardwareBackedKeys); - } -} diff --git a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/TryDecryptWithUnavailableGnuDummyKeyTest.java b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/TryDecryptWithUnavailableGnuDummyKeyTest.java new file mode 100644 index 00000000..79fa7a8e --- /dev/null +++ b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/TryDecryptWithUnavailableGnuDummyKeyTest.java @@ -0,0 +1,52 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.decryption_verification; + +import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.PGPPublicKeyRing; +import org.bouncycastle.openpgp.PGPSecretKeyRing; +import org.bouncycastle.util.io.Streams; +import org.junit.jupiter.api.Test; +import org.pgpainless.PGPainless; +import org.pgpainless.encryption_signing.EncryptionOptions; +import org.pgpainless.encryption_signing.EncryptionStream; +import org.pgpainless.encryption_signing.ProducerOptions; +import org.pgpainless.key.gnu_dummy_s2k.GnuPGDummyKeyUtil; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.security.InvalidAlgorithmParameterException; +import java.security.NoSuchAlgorithmException; + +import static org.junit.jupiter.api.Assertions.assertThrows; + +public class TryDecryptWithUnavailableGnuDummyKeyTest { + + @Test + public void testAttemptToDecryptWithRemovedPrivateKeysThrows() + throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, IOException { + PGPSecretKeyRing secretKeys = PGPainless.generateKeyRing() + .modernKeyRing("Hardy Hardware "); + PGPPublicKeyRing certificate = PGPainless.extractCertificate(secretKeys); + + ByteArrayOutputStream ciphertextOut = new ByteArrayOutputStream(); + EncryptionStream encryptionStream = PGPainless.encryptAndOrSign() + .onOutputStream(ciphertextOut) + .withOptions( + ProducerOptions.encrypt(EncryptionOptions.get().addRecipient(certificate))); + ByteArrayInputStream plaintextIn = new ByteArrayInputStream("Hello, World!\n".getBytes()); + Streams.pipeAll(plaintextIn, encryptionStream); + encryptionStream.close(); + + PGPSecretKeyRing removedKeys = GnuPGDummyKeyUtil.modify(secretKeys) + .removePrivateKeys(GnuPGDummyKeyUtil.KeyFilter.any()); + + ByteArrayInputStream ciphertextIn = new ByteArrayInputStream(ciphertextOut.toByteArray()); + assertThrows(PGPException.class, () -> PGPainless.decryptAndOrVerify() + .onInputStream(ciphertextIn) + .withOptions(ConsumerOptions.get().addDecryptionKey(removedKeys))); + } +} diff --git a/pgpainless-core/src/test/java/org/pgpainless/key/gnu_dummy_s2k/GnuPGDummyKeyUtilTest.java b/pgpainless-core/src/test/java/org/pgpainless/key/gnu_dummy_s2k/GnuPGDummyKeyUtilTest.java index 1fc3b892..e175e27f 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/key/gnu_dummy_s2k/GnuPGDummyKeyUtilTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/key/gnu_dummy_s2k/GnuPGDummyKeyUtilTest.java @@ -10,12 +10,17 @@ import org.bouncycastle.openpgp.PGPSecretKey; import org.bouncycastle.openpgp.PGPSecretKeyRing; import org.junit.jupiter.api.Test; import org.pgpainless.PGPainless; +import org.pgpainless.key.SubkeyIdentifier; import org.pgpainless.key.util.KeyIdUtil; import java.io.IOException; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; import static org.junit.jupiter.api.Assertions.assertArrayEquals; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; public class GnuPGDummyKeyUtilTest { // normal, non-hw-backed key @@ -73,6 +78,29 @@ public class GnuPGDummyKeyUtilTest { "=rYoa\n" + "-----END PGP PRIVATE KEY BLOCK-----"; + public static final String ALL_KEYS_REMOVED = "-----BEGIN PGP PRIVATE KEY BLOCK-----\n" + + "Version: PGPainless\n" + + "Comment: 01FD AB6C E04A 5078 79FE 4A18 C312 C97D A9F7 6A4F\n" + + "Comment: Hardy Hardware \n" + + "\n" + + "lDsEY1vSiBYJKwYBBAHaRw8BAQdAQ58lZn/HOtg+1b1KS18odyQ6M4LaDdbJAyRf\n" + + "eBwCeTT+AGUAR05VAbQgSGFyZHkgSGFyZHdhcmUgPGhhcmR5QGhhcmQud2FyZT6I\n" + + "jwQTFgoAQQUCY1vSiAkQwxLJfan3ak8WIQQB/ats4EpQeHn+ShjDEsl9qfdqTwKe\n" + + "AQKbAQUWAgMBAAQLCQgHBRUKCQgLApkBAAD5NgD/dtk+U0O4bpBZacV904TIYniZ\n" + + "xAhmORKreVNP7xGNV3YA/3hNTJfaqsekBnGjSnvHXjHtxIU2p7epkvbajB6dv94J\n" + + "nEAEY1vSiBIKKwYBBAGXVQEFAQEHQFVwSzbzZhYfSl+oi5nTSTNvGXPTxp8xKAA/\n" + + "fk+KdJQ8AwEIB/4AZQBHTlUBiHUEGBYKAB0FAmNb0ogCngECmwwFFgIDAQAECwkI\n" + + "BwUVCgkICwAKCRDDEsl9qfdqT8nJAP0YGPS+O1hkB/kWLR4Qp2ICCzTJmtA+Qyzp\n" + + "4v7ze17vvQD+MbQN4nL7zx859ZOP6aLE73w9k+dDQzJtYL/VBRO8/QGcOwRjW9KI\n" + + "FgkrBgEEAdpHDwEBB0C9JhMPrS3y/HXR1IQEAJSgh9UKl44HfQPqd/Am1sNPRv4A\n" + + "ZQBHTlUBiNUEGBYKAH0FAmNb0ogCngECmwIFFgIDAQAECwkIBwUVCgkIC18gBBkW\n" + + "CgAGBQJjW9KIAAoJEJQCL6VtwFtJDmMBAKqsGfRFQxJXyPgugWBgEaO5lt9fMM0y\n" + + "Uxa76cmSWe5fAQD2oLSEW1GOgIs64+Z3gvtXopmeupT09HhI7ger98zDAwAKCRDD\n" + + "Esl9qfdqTwR6AP9Xftw8xZ7/MWhYImk/xheqPy07K4qo3T1pGKUvUqjWQQEAhE3r\n" + + "0oTcJn+KVCwGjF6AYiLOzO/R1x5bSlYD3FeJ3Qo=\n" + + "=GEN/\n" + + "-----END PGP PRIVATE KEY BLOCK-----"; + public static final String PRIMARY_KEY_ON_CARD = "-----BEGIN PGP PRIVATE KEY BLOCK-----\n" + "Version: PGPainless\n" + "Comment: 01FD AB6C E04A 5078 79FE 4A18 C312 C97D A9F7 6A4F\n" + @@ -197,4 +225,53 @@ public class GnuPGDummyKeyUtilTest { assertArrayEquals(expected.getEncoded(), onCard.getEncoded()); } + + @Test + public void testRemoveAllKeys() throws IOException { + PGPSecretKeyRing secretKeys = PGPainless.readKeyRing().secretKeyRing(FULL_KEY); + PGPSecretKeyRing expected = PGPainless.readKeyRing().secretKeyRing(ALL_KEYS_REMOVED); + + PGPSecretKeyRing removedSecretKeys = GnuPGDummyKeyUtil.modify(secretKeys) + .removePrivateKeys(GnuPGDummyKeyUtil.KeyFilter.any()); + + for (PGPSecretKey key : removedSecretKeys) { + assertEquals(key.getS2KUsage(), SecretKeyPacket.USAGE_SHA1); + S2K s2k = key.getS2K(); + assertEquals(GnuPGDummyExtension.NO_PRIVATE_KEY.getId(), s2k.getProtectionMode()); + } + + assertArrayEquals(expected.getEncoded(), removedSecretKeys.getEncoded()); + } + + @Test + public void testGetSingleIdOfHardwareBackedKey() throws IOException { + PGPSecretKeyRing secretKeys = PGPainless.readKeyRing().secretKeyRing(FULL_KEY); + assertTrue(GnuPGDummyKeyUtil.getIdsOfKeysWithGnuPGS2KDivertedToCard(secretKeys).isEmpty()); + + PGPSecretKeyRing withHardwareBackedEncryptionKey = GnuPGDummyKeyUtil.modify(secretKeys) + .divertPrivateKeysToCard(GnuPGDummyKeyUtil.KeyFilter.only(encryptionKeyId)); + + Set hardwareBackedKeys = GnuPGDummyKeyUtil + .getIdsOfKeysWithGnuPGS2KDivertedToCard(withHardwareBackedEncryptionKey); + assertEquals(Collections.singleton(new SubkeyIdentifier(secretKeys, encryptionKeyId)), hardwareBackedKeys); + } + + + @Test + public void testGetIdsOfFullyHardwareBackedKey() throws IOException { + PGPSecretKeyRing secretKeys = PGPainless.readKeyRing().secretKeyRing(FULL_KEY); + assertTrue(GnuPGDummyKeyUtil.getIdsOfKeysWithGnuPGS2KDivertedToCard(secretKeys).isEmpty()); + + PGPSecretKeyRing withHardwareBackedEncryptionKey = GnuPGDummyKeyUtil.modify(secretKeys) + .divertPrivateKeysToCard(GnuPGDummyKeyUtil.KeyFilter.any()); + Set expected = new HashSet<>(); + for (PGPSecretKey key : secretKeys) { + expected.add(new SubkeyIdentifier(secretKeys, key.getKeyID())); + } + + Set hardwareBackedKeys = GnuPGDummyKeyUtil + .getIdsOfKeysWithGnuPGS2KDivertedToCard(withHardwareBackedEncryptionKey); + + assertEquals(expected, hardwareBackedKeys); + } } From 58aa9f571201e9df55d532e830287a26203899f6 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Sat, 29 Oct 2022 14:58:18 +0200 Subject: [PATCH 087/763] Move classes related to GNU dummy keys to gnupg package --- .../key/gnu_dummy_s2k => gnupg}/GnuPGDummyExtension.java | 2 +- .../key/gnu_dummy_s2k => gnupg}/GnuPGDummyKeyUtil.java | 2 +- .../{pgpainless/key/gnu_dummy_s2k => gnupg}/package-info.java | 2 +- .../key/gnu_dummy_s2k => gnupg}/GnuPGDummyKeyUtilTest.java | 4 +++- .../TryDecryptWithUnavailableGnuDummyKeyTest.java | 2 +- 5 files changed, 7 insertions(+), 5 deletions(-) rename pgpainless-core/src/main/java/org/{pgpainless/key/gnu_dummy_s2k => gnupg}/GnuPGDummyExtension.java (93%) rename pgpainless-core/src/main/java/org/{pgpainless/key/gnu_dummy_s2k => gnupg}/GnuPGDummyKeyUtil.java (99%) rename pgpainless-core/src/main/java/org/{pgpainless/key/gnu_dummy_s2k => gnupg}/package-info.java (81%) rename pgpainless-core/src/test/java/org/{pgpainless/key/gnu_dummy_s2k => gnupg}/GnuPGDummyKeyUtilTest.java (99%) diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/gnu_dummy_s2k/GnuPGDummyExtension.java b/pgpainless-core/src/main/java/org/gnupg/GnuPGDummyExtension.java similarity index 93% rename from pgpainless-core/src/main/java/org/pgpainless/key/gnu_dummy_s2k/GnuPGDummyExtension.java rename to pgpainless-core/src/main/java/org/gnupg/GnuPGDummyExtension.java index 9f75bf7e..d744e222 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/key/gnu_dummy_s2k/GnuPGDummyExtension.java +++ b/pgpainless-core/src/main/java/org/gnupg/GnuPGDummyExtension.java @@ -2,7 +2,7 @@ // // SPDX-License-Identifier: Apache-2.0 -package org.pgpainless.key.gnu_dummy_s2k; +package org.gnupg; import org.bouncycastle.bcpg.S2K; diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/gnu_dummy_s2k/GnuPGDummyKeyUtil.java b/pgpainless-core/src/main/java/org/gnupg/GnuPGDummyKeyUtil.java similarity index 99% rename from pgpainless-core/src/main/java/org/pgpainless/key/gnu_dummy_s2k/GnuPGDummyKeyUtil.java rename to pgpainless-core/src/main/java/org/gnupg/GnuPGDummyKeyUtil.java index 64c7ed26..983d8e68 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/key/gnu_dummy_s2k/GnuPGDummyKeyUtil.java +++ b/pgpainless-core/src/main/java/org/gnupg/GnuPGDummyKeyUtil.java @@ -2,7 +2,7 @@ // // SPDX-License-Identifier: Apache-2.0 -package org.pgpainless.key.gnu_dummy_s2k; +package org.gnupg; import org.bouncycastle.bcpg.PublicKeyPacket; import org.bouncycastle.bcpg.S2K; diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/gnu_dummy_s2k/package-info.java b/pgpainless-core/src/main/java/org/gnupg/package-info.java similarity index 81% rename from pgpainless-core/src/main/java/org/pgpainless/key/gnu_dummy_s2k/package-info.java rename to pgpainless-core/src/main/java/org/gnupg/package-info.java index 5c2a727e..03268619 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/key/gnu_dummy_s2k/package-info.java +++ b/pgpainless-core/src/main/java/org/gnupg/package-info.java @@ -5,4 +5,4 @@ /** * Utility classes related to creating keys with GNU DUMMY S2K values. */ -package org.pgpainless.key.gnu_dummy_s2k; +package org.gnupg; diff --git a/pgpainless-core/src/test/java/org/pgpainless/key/gnu_dummy_s2k/GnuPGDummyKeyUtilTest.java b/pgpainless-core/src/test/java/org/gnupg/GnuPGDummyKeyUtilTest.java similarity index 99% rename from pgpainless-core/src/test/java/org/pgpainless/key/gnu_dummy_s2k/GnuPGDummyKeyUtilTest.java rename to pgpainless-core/src/test/java/org/gnupg/GnuPGDummyKeyUtilTest.java index e175e27f..b9562f60 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/key/gnu_dummy_s2k/GnuPGDummyKeyUtilTest.java +++ b/pgpainless-core/src/test/java/org/gnupg/GnuPGDummyKeyUtilTest.java @@ -2,12 +2,14 @@ // // SPDX-License-Identifier: Apache-2.0 -package org.pgpainless.key.gnu_dummy_s2k; +package org.gnupg; import org.bouncycastle.bcpg.S2K; import org.bouncycastle.bcpg.SecretKeyPacket; import org.bouncycastle.openpgp.PGPSecretKey; import org.bouncycastle.openpgp.PGPSecretKeyRing; +import org.gnupg.GnuPGDummyExtension; +import org.gnupg.GnuPGDummyKeyUtil; import org.junit.jupiter.api.Test; import org.pgpainless.PGPainless; import org.pgpainless.key.SubkeyIdentifier; diff --git a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/TryDecryptWithUnavailableGnuDummyKeyTest.java b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/TryDecryptWithUnavailableGnuDummyKeyTest.java index 79fa7a8e..419c529d 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/TryDecryptWithUnavailableGnuDummyKeyTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/TryDecryptWithUnavailableGnuDummyKeyTest.java @@ -13,7 +13,7 @@ import org.pgpainless.PGPainless; import org.pgpainless.encryption_signing.EncryptionOptions; import org.pgpainless.encryption_signing.EncryptionStream; import org.pgpainless.encryption_signing.ProducerOptions; -import org.pgpainless.key.gnu_dummy_s2k.GnuPGDummyKeyUtil; +import org.gnupg.GnuPGDummyKeyUtil; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; From 58195c19b139f3fcd34693b0e7053529e8f59d17 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Sat, 29 Oct 2022 15:12:12 +0200 Subject: [PATCH 088/763] Properly handle failed decryption caused by removed private keys --- .../OpenPgpMessageInputStream.java | 25 +++++++++++++++++++ .../java/org/gnupg/GnuPGDummyKeyUtilTest.java | 24 ++++++++---------- ...DecryptWithUnavailableGnuDummyKeyTest.java | 3 ++- 3 files changed, 38 insertions(+), 14 deletions(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java index 1b11744c..a509cde2 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java @@ -18,6 +18,7 @@ import javax.annotation.Nonnull; import org.bouncycastle.bcpg.ArmoredInputStream; import org.bouncycastle.bcpg.BCPGInputStream; +import org.bouncycastle.bcpg.S2K; import org.bouncycastle.bcpg.UnsupportedPacketVersionException; import org.bouncycastle.openpgp.PGPCompressedData; import org.bouncycastle.openpgp.PGPEncryptedData; @@ -507,6 +508,14 @@ public class OpenPgpMessageInputStream extends DecryptionStream { } PGPSecretKey secretKey = decryptionKeys.getSecretKey(keyId); SubkeyIdentifier decryptionKeyId = new SubkeyIdentifier(decryptionKeys, secretKey.getKeyID()); + S2K s2K = secretKey.getS2K(); + if (s2K != null) { + int s2kType = s2K.getType(); + if (s2kType >= 100 && s2kType <= 110) { + LOGGER.debug("Skipping PKESK because key " + decryptionKeyId + " has unsupported private S2K specifier " + s2kType); + continue; + } + } LOGGER.debug("Attempt decryption using secret key " + decryptionKeyId); SecretKeyRingProtector protector = options.getSecretKeyProtector(decryptionKeys); @@ -532,6 +541,14 @@ public class OpenPgpMessageInputStream extends DecryptionStream { PGPSecretKeyRing decryptionKeys = decryptionKeyCandidate.getA(); PGPSecretKey secretKey = decryptionKeyCandidate.getB(); SubkeyIdentifier decryptionKeyId = new SubkeyIdentifier(decryptionKeys, secretKey.getKeyID()); + S2K s2K = secretKey.getS2K(); + if (s2K != null) { + int s2kType = s2K.getType(); + if (s2kType >= 100 && s2kType <= 110) { + LOGGER.debug("Skipping PKESK because key " + decryptionKeyId + " has unsupported private S2K specifier " + s2kType); + continue; + } + } LOGGER.debug("Attempt decryption of anonymous PKESK with key " + decryptionKeyId); SecretKeyRingProtector protector = options.getSecretKeyProtector(decryptionKeyCandidate.getA()); if (!protector.hasPassphraseFor(secretKey.getKeyID())) { @@ -567,6 +584,14 @@ public class OpenPgpMessageInputStream extends DecryptionStream { long keyId = secretKey.getKeyID(); PGPSecretKeyRing decryptionKey = getDecryptionKey(keyId); SubkeyIdentifier decryptionKeyId = new SubkeyIdentifier(decryptionKey, keyId); + S2K s2K = secretKey.getS2K(); + if (s2K != null) { + int s2kType = s2K.getType(); + if (s2kType >= 100 && s2kType <= 110) { + LOGGER.debug("Skipping PKESK because key " + decryptionKeyId + " has unsupported private S2K specifier " + s2kType); + continue; + } + } LOGGER.debug("Attempt decryption with key " + decryptionKeyId + " while interactively requesting its passphrase"); SecretKeyRingProtector protector = options.getSecretKeyProtector(decryptionKey); PGPPrivateKey privateKey = UnlockSecretKey.unlockSecretKey(secretKey, protector.getDecryptor(keyId)); diff --git a/pgpainless-core/src/test/java/org/gnupg/GnuPGDummyKeyUtilTest.java b/pgpainless-core/src/test/java/org/gnupg/GnuPGDummyKeyUtilTest.java index b9562f60..87c5b02e 100644 --- a/pgpainless-core/src/test/java/org/gnupg/GnuPGDummyKeyUtilTest.java +++ b/pgpainless-core/src/test/java/org/gnupg/GnuPGDummyKeyUtilTest.java @@ -4,25 +4,23 @@ package org.gnupg; -import org.bouncycastle.bcpg.S2K; -import org.bouncycastle.bcpg.SecretKeyPacket; -import org.bouncycastle.openpgp.PGPSecretKey; -import org.bouncycastle.openpgp.PGPSecretKeyRing; -import org.gnupg.GnuPGDummyExtension; -import org.gnupg.GnuPGDummyKeyUtil; -import org.junit.jupiter.api.Test; -import org.pgpainless.PGPainless; -import org.pgpainless.key.SubkeyIdentifier; -import org.pgpainless.key.util.KeyIdUtil; +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; import java.io.IOException; import java.util.Collections; import java.util.HashSet; import java.util.Set; -import static org.junit.jupiter.api.Assertions.assertArrayEquals; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertTrue; +import org.bouncycastle.bcpg.S2K; +import org.bouncycastle.bcpg.SecretKeyPacket; +import org.bouncycastle.openpgp.PGPSecretKey; +import org.bouncycastle.openpgp.PGPSecretKeyRing; +import org.junit.jupiter.api.Test; +import org.pgpainless.PGPainless; +import org.pgpainless.key.SubkeyIdentifier; +import org.pgpainless.key.util.KeyIdUtil; public class GnuPGDummyKeyUtilTest { // normal, non-hw-backed key diff --git a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/TryDecryptWithUnavailableGnuDummyKeyTest.java b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/TryDecryptWithUnavailableGnuDummyKeyTest.java index 419c529d..640025b1 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/TryDecryptWithUnavailableGnuDummyKeyTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/TryDecryptWithUnavailableGnuDummyKeyTest.java @@ -14,6 +14,7 @@ import org.pgpainless.encryption_signing.EncryptionOptions; import org.pgpainless.encryption_signing.EncryptionStream; import org.pgpainless.encryption_signing.ProducerOptions; import org.gnupg.GnuPGDummyKeyUtil; +import org.pgpainless.exception.MissingDecryptionMethodException; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; @@ -45,7 +46,7 @@ public class TryDecryptWithUnavailableGnuDummyKeyTest { .removePrivateKeys(GnuPGDummyKeyUtil.KeyFilter.any()); ByteArrayInputStream ciphertextIn = new ByteArrayInputStream(ciphertextOut.toByteArray()); - assertThrows(PGPException.class, () -> PGPainless.decryptAndOrVerify() + assertThrows(MissingDecryptionMethodException.class, () -> PGPainless.decryptAndOrVerify() .onInputStream(ciphertextIn) .withOptions(ConsumerOptions.get().addDecryptionKey(removedKeys))); } From ca49ed087b4f1bb7e416d0d636caaeb37d35e225 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Sat, 29 Oct 2022 15:50:34 +0200 Subject: [PATCH 089/763] Small clean-ups in OpenPgpMessageInputStream --- .../OpenPgpMessageInputStream.java | 108 +++++++----------- 1 file changed, 43 insertions(+), 65 deletions(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java index a509cde2..f2c30ced 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java @@ -109,7 +109,7 @@ public class OpenPgpMessageInputStream extends DecryptionStream { * @throws PGPException in case of an OpenPGP error */ public static OpenPgpMessageInputStream create(@Nonnull InputStream inputStream, - @Nonnull ConsumerOptions options) + @Nonnull ConsumerOptions options) throws IOException, PGPException { return create(inputStream, options, PGPainless.getPolicy()); } @@ -127,8 +127,8 @@ public class OpenPgpMessageInputStream extends DecryptionStream { * @throws IOException in case of an IO error */ public static OpenPgpMessageInputStream create(@Nonnull InputStream inputStream, - @Nonnull ConsumerOptions options, - @Nonnull Policy policy) + @Nonnull ConsumerOptions options, + @Nonnull Policy policy) throws PGPException, IOException { return create(inputStream, options, new MessageMetadata.Message(), policy); } @@ -479,9 +479,9 @@ public class OpenPgpMessageInputStream extends DecryptionStream { for (Passphrase passphrase : options.getDecryptionPassphrases()) { for (PGPPBEEncryptedData skesk : esks.skesks) { LOGGER.debug("Attempt decryption with provided passphrase"); - SymmetricKeyAlgorithm kekAlgorithm = SymmetricKeyAlgorithm.requireFromId(skesk.getAlgorithm()); + SymmetricKeyAlgorithm encapsulationAlgorithm = SymmetricKeyAlgorithm.requireFromId(skesk.getAlgorithm()); try { - throwIfUnacceptable(kekAlgorithm); + throwIfUnacceptable(encapsulationAlgorithm); } catch (UnacceptableAlgorithmException e) { LOGGER.debug("Skipping SKESK with unacceptable encapsulation algorithm", e); continue; @@ -489,7 +489,6 @@ public class OpenPgpMessageInputStream extends DecryptionStream { PBEDataDecryptorFactory decryptorFactory = ImplementationFactory.getInstance() .getPBEDataDecryptorFactory(passphrase); - if (decryptSKESKAndStream(skesk, decryptorFactory)) { return true; } @@ -497,10 +496,11 @@ public class OpenPgpMessageInputStream extends DecryptionStream { } List> postponedDueToMissingPassphrase = new ArrayList<>(); + // Try (known) secret keys for (PGPPublicKeyEncryptedData pkesk : esks.pkesks) { long keyId = pkesk.getKeyID(); - LOGGER.debug("Encountered PKESK with recipient " + KeyIdUtil.formatKeyId(keyId)); + LOGGER.debug("Encountered PKESK for recipient " + KeyIdUtil.formatKeyId(keyId)); PGPSecretKeyRing decryptionKeys = getDecryptionKey(keyId); if (decryptionKeys == null) { LOGGER.debug("Skipping PKESK because no matching key " + KeyIdUtil.formatKeyId(keyId) + " was provided"); @@ -508,13 +508,8 @@ public class OpenPgpMessageInputStream extends DecryptionStream { } PGPSecretKey secretKey = decryptionKeys.getSecretKey(keyId); SubkeyIdentifier decryptionKeyId = new SubkeyIdentifier(decryptionKeys, secretKey.getKeyID()); - S2K s2K = secretKey.getS2K(); - if (s2K != null) { - int s2kType = s2K.getType(); - if (s2kType >= 100 && s2kType <= 110) { - LOGGER.debug("Skipping PKESK because key " + decryptionKeyId + " has unsupported private S2K specifier " + s2kType); - continue; - } + if (hasUnsupportedS2KSpecifier(secretKey, decryptionKeyId)) { + continue; } LOGGER.debug("Attempt decryption using secret key " + decryptionKeyId); @@ -527,10 +522,7 @@ public class OpenPgpMessageInputStream extends DecryptionStream { } PGPPrivateKey privateKey = UnlockSecretKey.unlockSecretKey(secretKey, protector); - - PublicKeyDataDecryptorFactory decryptorFactory = ImplementationFactory.getInstance() - .getPublicKeyDataDecryptorFactory(privateKey); - if (decryptPKESKAndStream(decryptionKeyId, decryptorFactory, pkesk)) { + if (decryptWithPrivateKey(privateKey, decryptionKeyId, pkesk)) { return true; } } @@ -541,13 +533,8 @@ public class OpenPgpMessageInputStream extends DecryptionStream { PGPSecretKeyRing decryptionKeys = decryptionKeyCandidate.getA(); PGPSecretKey secretKey = decryptionKeyCandidate.getB(); SubkeyIdentifier decryptionKeyId = new SubkeyIdentifier(decryptionKeys, secretKey.getKeyID()); - S2K s2K = secretKey.getS2K(); - if (s2K != null) { - int s2kType = s2K.getType(); - if (s2kType >= 100 && s2kType <= 110) { - LOGGER.debug("Skipping PKESK because key " + decryptionKeyId + " has unsupported private S2K specifier " + s2kType); - continue; - } + if (hasUnsupportedS2KSpecifier(secretKey, decryptionKeyId)) { + continue; } LOGGER.debug("Attempt decryption of anonymous PKESK with key " + decryptionKeyId); SecretKeyRingProtector protector = options.getSecretKeyProtector(decryptionKeyCandidate.getA()); @@ -556,11 +543,9 @@ public class OpenPgpMessageInputStream extends DecryptionStream { postponedDueToMissingPassphrase.add(new Tuple<>(secretKey, pkesk)); continue; } - PGPPrivateKey privateKey = UnlockSecretKey.unlockSecretKey(decryptionKeyCandidate.getB(), protector); - PublicKeyDataDecryptorFactory decryptorFactory = ImplementationFactory.getInstance() - .getPublicKeyDataDecryptorFactory(privateKey); - if (decryptPKESKAndStream(decryptionKeyId, decryptorFactory, pkesk)) { + PGPPrivateKey privateKey = UnlockSecretKey.unlockSecretKey(secretKey, protector); + if (decryptWithPrivateKey(privateKey, decryptionKeyId, pkesk)) { return true; } } @@ -571,7 +556,8 @@ public class OpenPgpMessageInputStream extends DecryptionStream { Set keyIds = new HashSet<>(); for (Tuple k : postponedDueToMissingPassphrase) { PGPSecretKey key = k.getA(); - keyIds.add(new SubkeyIdentifier(getDecryptionKey(key.getKeyID()), key.getKeyID())); + PGPSecretKeyRing keys = getDecryptionKey(key.getKeyID()); + keyIds.add(new SubkeyIdentifier(keys, key.getKeyID())); } if (!keyIds.isEmpty()) { throw new MissingPassphraseException(keyIds); @@ -584,21 +570,14 @@ public class OpenPgpMessageInputStream extends DecryptionStream { long keyId = secretKey.getKeyID(); PGPSecretKeyRing decryptionKey = getDecryptionKey(keyId); SubkeyIdentifier decryptionKeyId = new SubkeyIdentifier(decryptionKey, keyId); - S2K s2K = secretKey.getS2K(); - if (s2K != null) { - int s2kType = s2K.getType(); - if (s2kType >= 100 && s2kType <= 110) { - LOGGER.debug("Skipping PKESK because key " + decryptionKeyId + " has unsupported private S2K specifier " + s2kType); - continue; - } + if (hasUnsupportedS2KSpecifier(secretKey, decryptionKeyId)) { + continue; } + LOGGER.debug("Attempt decryption with key " + decryptionKeyId + " while interactively requesting its passphrase"); SecretKeyRingProtector protector = options.getSecretKeyProtector(decryptionKey); - PGPPrivateKey privateKey = UnlockSecretKey.unlockSecretKey(secretKey, protector.getDecryptor(keyId)); - PublicKeyDataDecryptorFactory decryptorFactory = ImplementationFactory.getInstance() - .getPublicKeyDataDecryptorFactory(privateKey); - - if (decryptPKESKAndStream(decryptionKeyId, decryptorFactory, pkesk)) { + PGPPrivateKey privateKey = UnlockSecretKey.unlockSecretKey(secretKey, protector); + if (decryptWithPrivateKey(privateKey, decryptionKeyId, pkesk)) { return true; } } @@ -613,6 +592,27 @@ public class OpenPgpMessageInputStream extends DecryptionStream { return false; } + private boolean decryptWithPrivateKey(PGPPrivateKey privateKey, + SubkeyIdentifier decryptionKeyId, + PGPPublicKeyEncryptedData pkesk) + throws PGPException, IOException { + PublicKeyDataDecryptorFactory decryptorFactory = ImplementationFactory.getInstance() + .getPublicKeyDataDecryptorFactory(privateKey); + return decryptPKESKAndStream(decryptionKeyId, decryptorFactory, pkesk); + } + + private static boolean hasUnsupportedS2KSpecifier(PGPSecretKey secretKey, SubkeyIdentifier decryptionKeyId) { + S2K s2K = secretKey.getS2K(); + if (s2K != null) { + int s2kType = s2K.getType(); + if (s2kType >= 100 && s2kType <= 110) { + LOGGER.debug("Skipping PKESK because key " + decryptionKeyId + " has unsupported private S2K specifier " + s2kType); + return true; + } + } + return false; + } + private boolean decryptSKESKAndStream(PGPPBEEncryptedData skesk, PBEDataDecryptorFactory decryptorFactory) throws IOException, UnacceptableAlgorithmException { try { @@ -661,14 +661,6 @@ public class OpenPgpMessageInputStream extends DecryptionStream { return false; } - private PGPSecretKey getDecryptionKey(PGPSecretKeyRing decryptionKeys, long keyId) { - KeyRingInfo info = PGPainless.inspectKeyRing(decryptionKeys); - if (info.getEncryptionSubkeys(EncryptionPurpose.ANY).contains(info.getPublicKey(keyId))) { - return info.getSecretKey(keyId); - } - return null; - } - private void throwIfUnacceptable(SymmetricKeyAlgorithm algorithm) throws UnacceptableAlgorithmException { if (!policy.getSymmetricKeyDecryptionAlgorithmPolicy().isAcceptable(algorithm)) { @@ -883,20 +875,6 @@ public class OpenPgpMessageInputStream extends DecryptionStream { return resultBuilder.build(); } - static void log(String message) { - LOGGER.debug(message); - // CHECKSTYLE:OFF - System.out.println(message); - // CHECKSTYLE:ON - } - - static void log(String message, Throwable e) { - log(message); - // CHECKSTYLE:OFF - e.printStackTrace(); - // CHECKSTYLE:ON - } - // In 'OPS LIT("Foo") SIG', OPS is only updated with "Foo" // In 'OPS[1] OPS LIT("Foo") SIG SIG', OPS[1] (nested) is updated with OPS LIT("Foo") SIG. // Therefore, we need to handle the innermost signature layer differently when updating with Literal data. @@ -1004,7 +982,7 @@ public class OpenPgpMessageInputStream extends DecryptionStream { try { SignatureValidator.signatureWasCreatedInBounds(options.getVerifyNotBefore(), options.getVerifyNotAfter()) - .verify(signature); + .verify(signature); CertificateValidator.validateCertificateAndVerifyOnePassSignature(onePassSignature, policy); LOGGER.debug("Acceptable signature by key " + verification.getSigningKey()); layer.addVerifiedOnePassSignature(verification); From f86aae499726e715ef1dd793314014c1bbdb2050 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 2 Nov 2022 10:37:24 +0100 Subject: [PATCH 090/763] Implement efficient read(buf,off,len) for DelayedInputStream --- .../decryption_verification/TeeBCPGInputStream.java | 4 ---- 1 file changed, 4 deletions(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/TeeBCPGInputStream.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/TeeBCPGInputStream.java index 52ec9001..28b415e0 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/TeeBCPGInputStream.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/TeeBCPGInputStream.java @@ -127,9 +127,6 @@ public class TeeBCPGInputStream { } } - // TODO: Uncomment, once BC-172.1 is available - // see https://github.com/bcgit/bc-java/issues/1257 - /* @Override public int read(byte[] b, int off, int len) throws IOException { if (last != -1) { @@ -145,7 +142,6 @@ public class TeeBCPGInputStream { } return r; } - */ /** * Squeeze the last byte out and update the output stream. From b1f9a1398a879ef0d8ed2a1b4301618cade9df5e Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 3 Nov 2022 12:07:26 +0100 Subject: [PATCH 091/763] Add comment for ArmorUtils method --- .../src/main/java/org/pgpainless/util/ArmorUtils.java | 1 + 1 file changed, 1 insertion(+) diff --git a/pgpainless-core/src/main/java/org/pgpainless/util/ArmorUtils.java b/pgpainless-core/src/main/java/org/pgpainless/util/ArmorUtils.java index 8a51ff3d..b3e60023 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/util/ArmorUtils.java +++ b/pgpainless-core/src/main/java/org/pgpainless/util/ArmorUtils.java @@ -178,6 +178,7 @@ public final class ArmorUtils { * If it is
false
, the signature will be encoded as-is. * * @param signature signature + * @param export whether to exclude non-exportable subpackets or trust-packets. * @return ascii armored string * * @throws IOException in case of an error in the {@link ArmoredOutputStream} From 313dbcfaa88d1bacc6f38f2af676f58d1157e9c7 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 3 Nov 2022 12:29:50 +0100 Subject: [PATCH 092/763] Update changelog --- CHANGELOG.md | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 551728aa..af1029ce 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,35 @@ SPDX-License-Identifier: CC0-1.0 # PGPainless Changelog +## 1.4.0-SNAPSHOT +- Reimplement message consumption via new `OpenPgpMessageInputStream` + - Fix validation of prepended signatures (#314) + - Fix validation of nested signatures (#319) + - Reject malformed messages (#237) + - Utilize new `PDA` syntax verifier class + - Allow for custom message syntax via `Syntax` class + - Gracefully handle `UnsupportedPacketVersionException` for signatures + - Allow plugin decryption code (e.g. to add support for hardware-backed keys (see #318)) + - Add `HardwareSecurity` utility class + - Add `GnuPGDummyKeyUtil` which can be used to mimic GnuPGs proprietary S2K extensions + for keys which were placed on hardware tokens + - Add `OpenPgpPacket` enum class to enumerate available packet tags + - Remove old decryption classes in favor of new implementation + - Removed `DecryptionStream` class and replaced with new abstract class + - Removed `DecryptionStreamFactory` + - Removed `FinalIOException` + - Removed `MissingLiteralDataException` (replaced by `MalformedOpenPgpMessageException`) + - Introduce `MessageMetadata` class as potential future replacement for `OpenPgpMetadata`. + - can be obtained via `((OpenPgpMessageInputStream) decryptionStream).getMetadata();` +- Add `CachingBcPublicKeyDataDecryptorFactory` which can be extended to prevent costly decryption + of session keys +- Fix: Only verify message integrity once +- Remove unnecessary `@throws` declarations on `KeyRingReader` methods +- Remove unnecessary `@throws` declarations on `KeyRingUtils` methods +- Add `KeyIdUtil.formatKeyId(long id)` to format hexadecimal key-ids. +- Add `KeyRingUtils.publicKeys(PGPKeyRing keys)` +- Remove `BCUtil` class + ## 1.3.13 - Bump `sop-java` to `4.0.7` From 256920bfae35da2009ba328d57d04862f567b949 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 3 Nov 2022 12:34:52 +0100 Subject: [PATCH 093/763] PGPainless 1.4.0-rc1 --- CHANGELOG.md | 2 +- README.md | 2 +- pgpainless-sop/README.md | 4 ++-- version.gradle | 4 ++-- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index af1029ce..54b9b8a4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ SPDX-License-Identifier: CC0-1.0 # PGPainless Changelog -## 1.4.0-SNAPSHOT +## 1.4.0-rc1 - Reimplement message consumption via new `OpenPgpMessageInputStream` - Fix validation of prepended signatures (#314) - Fix validation of nested signatures (#319) diff --git a/README.md b/README.md index 90f6be81..1855f742 100644 --- a/README.md +++ b/README.md @@ -191,7 +191,7 @@ repositories { } dependencies { - implementation 'org.pgpainless:pgpainless-core:1.3.13' + implementation 'org.pgpainless:pgpainless-core:1.4.0-rc1' } ``` diff --git a/pgpainless-sop/README.md b/pgpainless-sop/README.md index 4a18136d..893804dd 100644 --- a/pgpainless-sop/README.md +++ b/pgpainless-sop/README.md @@ -23,7 +23,7 @@ To start using pgpainless-sop in your code, include the following lines in your ... dependencies { ... - implementation "org.pgpainless:pgpainless-sop:1.3.13" + implementation "org.pgpainless:pgpainless-sop:1.4.0-rc1" ... } @@ -34,7 +34,7 @@ dependencies { org.pgpainless pgpainless-sop - 1.3.13 + 1.4.0-rc1 ... diff --git a/version.gradle b/version.gradle index 7d25632e..d99e2f6b 100644 --- a/version.gradle +++ b/version.gradle @@ -4,8 +4,8 @@ allprojects { ext { - shortVersion = '1.4.0' - isSnapshot = true + shortVersion = '1.4.0-rc1' + isSnapshot = false pgpainlessMinAndroidSdk = 10 javaSourceCompatibility = 1.8 bouncyCastleVersion = '1.72' From 6243d690614eb10cb22fa378d59b647cf5a67f09 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 3 Nov 2022 12:38:38 +0100 Subject: [PATCH 094/763] PGPainless 1.4.0-rc2-SNAPSHOT --- version.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/version.gradle b/version.gradle index d99e2f6b..d6634b93 100644 --- a/version.gradle +++ b/version.gradle @@ -4,8 +4,8 @@ allprojects { ext { - shortVersion = '1.4.0-rc1' - isSnapshot = false + shortVersion = '1.4.0-rc2' + isSnapshot = true pgpainlessMinAndroidSdk = 10 javaSourceCompatibility = 1.8 bouncyCastleVersion = '1.72' From f80b3e0cdb4d2ef6b5cb34229a9af06d44532960 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 3 Nov 2022 12:52:20 +0100 Subject: [PATCH 095/763] Use BCs PGPEncryptedDataList.isIntegrityProtected() --- .../decryption_verification/OpenPgpMessageInputStream.java | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java index f2c30ced..83c0a3f2 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java @@ -405,9 +405,7 @@ public class OpenPgpMessageInputStream extends DecryptionStream { syntaxVerifier.next(InputSymbol.EncryptedData); PGPEncryptedDataList encDataList = packetInputStream.readEncryptedDataList(); - // TODO: Replace with !encDataList.isIntegrityProtected() - // once BC ships it - if (!encDataList.get(0).isIntegrityProtected()) { + if (!encDataList.isIntegrityProtected()) { LOGGER.debug("Symmetrically Encrypted Data Packet is not integrity-protected and is therefore rejected."); throw new MessageNotIntegrityProtectedException(); } From 59e81dc514018b4e1705decdc0e375e34b43bb33 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 3 Nov 2022 13:04:13 +0100 Subject: [PATCH 096/763] Use BCs PGPEncryptedDataList.extractSessionKeyEncryptedData() for decryption with session key --- .../OpenPgpMessageInputStream.java | 30 +++++-------------- 1 file changed, 8 insertions(+), 22 deletions(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java index 83c0a3f2..28440257 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java @@ -33,6 +33,7 @@ import org.bouncycastle.openpgp.PGPPublicKeyEncryptedData; import org.bouncycastle.openpgp.PGPPublicKeyRing; import org.bouncycastle.openpgp.PGPSecretKey; import org.bouncycastle.openpgp.PGPSecretKeyRing; +import org.bouncycastle.openpgp.PGPSessionKeyEncryptedData; import org.bouncycastle.openpgp.PGPSignature; import org.bouncycastle.openpgp.PGPSignatureList; import org.bouncycastle.openpgp.operator.PBEDataDecryptorFactory; @@ -444,29 +445,14 @@ public class OpenPgpMessageInputStream extends DecryptionStream { MessageMetadata.EncryptedData encryptedData = new MessageMetadata.EncryptedData( sessionKey.getAlgorithm(), metadata.depth + 1); + PGPSessionKeyEncryptedData sessionKeyEncryptedData = encDataList.extractSessionKeyEncryptedData(); try { - // TODO: Use BCs new API once shipped - // see https://github.com/bcgit/bc-java/pull/1228 (discussion) - PGPEncryptedData esk = esks.all().get(0); - if (esk instanceof PGPPBEEncryptedData) { - PGPPBEEncryptedData skesk = (PGPPBEEncryptedData) esk; - InputStream decrypted = skesk.getDataStream(decryptorFactory); - encryptedData.sessionKey = sessionKey; - IntegrityProtectedInputStream integrityProtected = new IntegrityProtectedInputStream(decrypted, skesk, options); - nestedInputStream = new OpenPgpMessageInputStream(integrityProtected, options, encryptedData, policy); - LOGGER.debug("Successfully decrypted data with provided session key"); - return true; - } else if (esk instanceof PGPPublicKeyEncryptedData) { - PGPPublicKeyEncryptedData pkesk = (PGPPublicKeyEncryptedData) esk; - InputStream decrypted = pkesk.getDataStream(decryptorFactory); - encryptedData.sessionKey = sessionKey; - IntegrityProtectedInputStream integrityProtected = new IntegrityProtectedInputStream(decrypted, pkesk, options); - nestedInputStream = new OpenPgpMessageInputStream(integrityProtected, options, encryptedData, policy); - LOGGER.debug("Successfully decrypted data with provided session key"); - return true; - } else { - throw new RuntimeException("Unknown ESK class type: " + esk.getClass().getName()); - } + InputStream decrypted = sessionKeyEncryptedData.getDataStream(decryptorFactory); + encryptedData.sessionKey = sessionKey; + IntegrityProtectedInputStream integrityProtected = new IntegrityProtectedInputStream(decrypted, sessionKeyEncryptedData, options); + nestedInputStream = new OpenPgpMessageInputStream(integrityProtected, options, encryptedData, policy); + LOGGER.debug("Successfully decrypted data with provided session key"); + return true; } catch (PGPException e) { // Session key mismatch? LOGGER.debug("Decryption using provided session key failed. Mismatched session key and message?", e); From 963b678a9e5ec5b98e50e1a68bf2a391e6e8f8c4 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 3 Nov 2022 13:04:36 +0100 Subject: [PATCH 097/763] Enable test for decryption of messages without ESKs --- .../TestDecryptionOfMessageWithoutESKUsingSessionKey.java | 3 --- 1 file changed, 3 deletions(-) diff --git a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/TestDecryptionOfMessageWithoutESKUsingSessionKey.java b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/TestDecryptionOfMessageWithoutESKUsingSessionKey.java index c9778426..b28af822 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/TestDecryptionOfMessageWithoutESKUsingSessionKey.java +++ b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/TestDecryptionOfMessageWithoutESKUsingSessionKey.java @@ -7,7 +7,6 @@ package org.pgpainless.decryption_verification; import org.bouncycastle.openpgp.PGPException; import org.bouncycastle.openpgp.PGPSessionKey; import org.bouncycastle.util.io.Streams; -import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.pgpainless.PGPainless; import org.pgpainless.util.SessionKey; @@ -55,8 +54,6 @@ public class TestDecryptionOfMessageWithoutESKUsingSessionKey { assertEquals("Hello, World!\n", out.toString()); } - // TODO: Enable when BC 173 gets released with our fix - @Disabled("Bug in BC 172. See https://github.com/bcgit/bc-java/pull/1228") @Test public void decryptMessageWithoutSKESK() throws PGPException, IOException { ByteArrayInputStream in = new ByteArrayInputStream(encryptedMessageWithoutESK.getBytes(StandardCharsets.UTF_8)); From 6dcc1e68cd2ea4969caf0a7a94bb1fdb925f5ff0 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 3 Nov 2022 13:04:53 +0100 Subject: [PATCH 098/763] Fix expected exception in roundtrip test --- .../java/org/pgpainless/sop/EncryptDecryptRoundTripTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pgpainless-sop/src/test/java/org/pgpainless/sop/EncryptDecryptRoundTripTest.java b/pgpainless-sop/src/test/java/org/pgpainless/sop/EncryptDecryptRoundTripTest.java index 515cf71d..22af5b70 100644 --- a/pgpainless-sop/src/test/java/org/pgpainless/sop/EncryptDecryptRoundTripTest.java +++ b/pgpainless-sop/src/test/java/org/pgpainless/sop/EncryptDecryptRoundTripTest.java @@ -512,7 +512,7 @@ public class EncryptDecryptRoundTripTest { "-----END PGP MESSAGE-----").getBytes(StandardCharsets.UTF_8); SessionKey wrongSessionKey = SessionKey.fromString("9:63F741E7FB60247BE59C64158573308F727236482DB7653908C95839E4166AAE"); - assertThrows(SOPGPException.BadData.class, () -> + assertThrows(SOPGPException.CannotDecrypt.class, () -> sop.decrypt().withSessionKey(wrongSessionKey).ciphertext(ciphertext)); } From 2dc72d76908f8f00200b187d2b95751722731326 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 3 Nov 2022 13:14:58 +0100 Subject: [PATCH 099/763] Update CHANGELOG --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 54b9b8a4..cff696f1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,12 @@ SPDX-License-Identifier: CC0-1.0 # PGPainless Changelog +## 1.4.0-rc2-SNAPSHOT +- Use BCs `PGPEncryptedDataList.extractSessionKeyEncryptedData()` method + to do decryption using session keys. This enables decryption of messages + without encrypted session key packets. +- Use BCs `PGPEncryptedDataList.isIntegrityProtected()` to check for integrity protection + ## 1.4.0-rc1 - Reimplement message consumption via new `OpenPgpMessageInputStream` - Fix validation of prepended signatures (#314) From 552459608234e4c4edc9b9b50f7ab5fd91d82947 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Sun, 6 Nov 2022 18:53:40 +0100 Subject: [PATCH 100/763] Add more tests for sop code --- .../cli/commands/DearmorCmdTest.java | 13 +++ .../RoundTripInlineSignVerifyCmdTest.java | 107 ++++++++++++++++++ 2 files changed, 120 insertions(+) create mode 100644 pgpainless-cli/src/test/java/org/pgpainless/cli/commands/RoundTripInlineSignVerifyCmdTest.java diff --git a/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/DearmorCmdTest.java b/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/DearmorCmdTest.java index 4ebaf21d..b8e5532f 100644 --- a/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/DearmorCmdTest.java +++ b/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/DearmorCmdTest.java @@ -6,6 +6,7 @@ package org.pgpainless.cli.commands; import static org.junit.jupiter.api.Assertions.assertArrayEquals; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; import java.io.ByteArrayOutputStream; import java.io.IOException; @@ -87,4 +88,16 @@ public class DearmorCmdTest extends CLITest { assertEquals("Hello, World\n", out.toString()); } + + @Test + public void dearmorGarbageEmitsEmpty() { + String noArmoredData = "This is not armored."; + System.setIn(new ByteArrayInputStream(noArmoredData.getBytes(StandardCharsets.UTF_8))); + ByteArrayOutputStream out = new ByteArrayOutputStream(); + System.setOut(new PrintStream(out)); + PGPainlessCLI.execute("dearmor"); + + assertTrue(out.toString().isEmpty()); + } + } diff --git a/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/RoundTripInlineSignVerifyCmdTest.java b/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/RoundTripInlineSignVerifyCmdTest.java new file mode 100644 index 00000000..82fda430 --- /dev/null +++ b/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/RoundTripInlineSignVerifyCmdTest.java @@ -0,0 +1,107 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.cli.commands; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.io.PrintStream; +import java.nio.charset.StandardCharsets; + +import com.ginsberg.junit.exit.FailOnSystemExit; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.pgpainless.cli.PGPainlessCLI; +import org.pgpainless.cli.TestUtils; + +public class RoundTripInlineSignVerifyCmdTest { + private static File tempDir; + private static PrintStream originalSout; + + @BeforeAll + public static void prepare() throws IOException { + tempDir = TestUtils.createTempDirectory(); + } + + @Test + @FailOnSystemExit + public void encryptAndDecryptAMessage() throws IOException { + originalSout = System.out; + File sigmundKeyFile = new File(tempDir, "sigmund.key"); + assertTrue(sigmundKeyFile.createNewFile()); + + File sigmundCertFile = new File(tempDir, "sigmund.cert"); + assertTrue(sigmundCertFile.createNewFile()); + + File msgFile = new File(tempDir, "signed.asc"); + assertTrue(msgFile.createNewFile()); + + File passwordFile = new File(tempDir, "password"); + assertTrue(passwordFile.createNewFile()); + + // write password file + FileOutputStream passwordOut = new FileOutputStream(passwordFile); + passwordOut.write("sw0rdf1sh".getBytes(StandardCharsets.UTF_8)); + passwordOut.close(); + + // generate key + OutputStream sigmundKeyOut = new FileOutputStream(sigmundKeyFile); + System.setOut(new PrintStream(sigmundKeyOut)); + PGPainlessCLI.execute("generate-key", + "--with-key-password=" + passwordFile.getAbsolutePath(), + "Sigmund Freud "); + sigmundKeyOut.close(); + + // extract cert + FileInputStream sigmundKeyIn = new FileInputStream(sigmundKeyFile); + System.setIn(sigmundKeyIn); + OutputStream sigmundCertOut = new FileOutputStream(sigmundCertFile); + System.setOut(new PrintStream(sigmundCertOut)); + PGPainlessCLI.execute("extract-cert"); + sigmundKeyIn.close(); + sigmundCertOut.close(); + + // sign message + String msg = "Hello World!\n"; + ByteArrayInputStream msgIn = new ByteArrayInputStream(msg.getBytes(StandardCharsets.UTF_8)); + System.setIn(msgIn); + OutputStream msgAscOut = new FileOutputStream(msgFile); + System.setOut(new PrintStream(msgAscOut)); + PGPainlessCLI.execute("inline-sign", + "--with-key-password=" + passwordFile.getAbsolutePath(), + sigmundKeyFile.getAbsolutePath()); + msgAscOut.close(); + + File verifyFile = new File(tempDir, "verify.txt"); + + FileInputStream msgAscIn = new FileInputStream(msgFile); + System.setIn(msgAscIn); + ByteArrayOutputStream out = new ByteArrayOutputStream(); + PrintStream pOut = new PrintStream(out); + System.setOut(pOut); + PGPainlessCLI.execute("inline-verify", + "--verifications-out", verifyFile.getAbsolutePath(), + sigmundCertFile.getAbsolutePath()); + msgAscIn.close(); + + assertEquals(msg, out.toString()); + } + + @AfterAll + public static void after() { + System.setOut(originalSout); + // CHECKSTYLE:OFF + System.out.println(tempDir.getAbsolutePath()); + // CHECKSTYLE:ON + } +} From 86c72291727e8f202a1101d48047e90648480494 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 9 Nov 2022 22:23:24 +0100 Subject: [PATCH 101/763] Do not reject bnacksig signatures when they predate subkey binding date Fixes #334 SOP verify: force data to be non-openpgp data Update changelog SOP: Unify key/certificate reading code Fix key/password matching in SOPs detached sign command Rework CLI tests update changelog PGPainless 1.3.11 PGPainless 1.3.12-SNAPSHOT Merge branch 'release/1.3' --- .../org/pgpainless/cli/commands/DearmorCmdTest.java | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/DearmorCmdTest.java b/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/DearmorCmdTest.java index b8e5532f..8fcf093d 100644 --- a/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/DearmorCmdTest.java +++ b/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/DearmorCmdTest.java @@ -90,13 +90,11 @@ public class DearmorCmdTest extends CLITest { } @Test - public void dearmorGarbageEmitsEmpty() { + public void dearmorGarbageEmitsEmpty() throws IOException { String noArmoredData = "This is not armored."; - System.setIn(new ByteArrayInputStream(noArmoredData.getBytes(StandardCharsets.UTF_8))); - ByteArrayOutputStream out = new ByteArrayOutputStream(); - System.setOut(new PrintStream(out)); - PGPainlessCLI.execute("dearmor"); - + pipeStringToStdin(noArmoredData); + ByteArrayOutputStream out = pipeStdoutToStream(); + assertSuccess(executeCommand("dearmor")); assertTrue(out.toString().isEmpty()); } From b287d28a28bf07cfc4aaa6c51fb3fdca4029d4a1 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 9 Aug 2022 15:10:59 +0200 Subject: [PATCH 102/763] Depend on pgp-certificate-store --- pgpainless-core/build.gradle | 3 +++ version.gradle | 1 + 2 files changed, 4 insertions(+) diff --git a/pgpainless-core/build.gradle b/pgpainless-core/build.gradle index 3c73121f..544fb8a4 100644 --- a/pgpainless-core/build.gradle +++ b/pgpainless-core/build.gradle @@ -24,6 +24,9 @@ dependencies { api "org.bouncycastle:bcpg-jdk15to18:$bouncyPgVersion" // api(files("../libs/bcpg-jdk18on-1.70.jar")) + // certificate store + api "org.pgpainless:pgp-certificate-store:$pgpCertificateStoreVersion" + // @Nullable, @Nonnull annotations implementation "com.google.code.findbugs:jsr305:3.0.2" } diff --git a/version.gradle b/version.gradle index d6634b93..302f54e0 100644 --- a/version.gradle +++ b/version.gradle @@ -17,6 +17,7 @@ allprojects { junitVersion = '5.8.2' logbackVersion = '1.2.11' mockitoVersion = '4.5.1' + pgpCertificateStoreVersion = '0.1.1' slf4jVersion = '1.7.36' sopJavaVersion = '4.0.7' } From d486a17cf1b864d9d37073bd9e7cb891654daf14 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 9 Aug 2022 15:11:18 +0200 Subject: [PATCH 103/763] Implement EncryptionOptions.addRecipient(store, fingerprint) --- .../encryption_signing/EncryptionOptions.java | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/EncryptionOptions.java b/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/EncryptionOptions.java index 6d2ca642..4bf21434 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/EncryptionOptions.java +++ b/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/EncryptionOptions.java @@ -4,6 +4,7 @@ package org.pgpainless.encryption_signing; +import java.io.IOException; import java.util.Collections; import java.util.Date; import java.util.HashMap; @@ -21,6 +22,7 @@ import org.bouncycastle.openpgp.PGPPublicKeyRing; import org.bouncycastle.openpgp.PGPPublicKeyRingCollection; import org.bouncycastle.openpgp.operator.PBEKeyEncryptionMethodGenerator; import org.bouncycastle.openpgp.operator.PGPKeyEncryptionMethodGenerator; +import org.pgpainless.PGPainless; import org.pgpainless.algorithm.EncryptionPurpose; import org.pgpainless.algorithm.SymmetricKeyAlgorithm; import org.pgpainless.exception.KeyException; @@ -30,6 +32,10 @@ import org.pgpainless.key.SubkeyIdentifier; import org.pgpainless.key.info.KeyAccessor; import org.pgpainless.key.info.KeyRingInfo; import org.pgpainless.util.Passphrase; +import pgp.certificate_store.Certificate; +import pgp.certificate_store.CertificateStore; +import pgp.certificate_store.exception.BadDataException; +import pgp.certificate_store.exception.BadNameException; /** * Options for the encryption process. @@ -235,6 +241,30 @@ public class EncryptionOptions { return this; } + /** + * Add a recipient by providing a {@link CertificateStore} and the {@link OpenPgpFingerprint} of the recipients key. + * If no such certificate is found in the store, a {@link NoSuchElementException is thrown}. + * + * @param certificateStore certificate store + * @param certificateFingerprint fingerprint of the recipient certificate + * @return builder + * @throws BadDataException if the certificate contains bad data + * @throws BadNameException if the fingerprint is not in a recognizable form for the store + * @throws IOException in case of an IO error + * @throws NoSuchElementException if the store does not contain a certificate for the given fingerprint + */ + public EncryptionOptions addRecipient(@Nonnull CertificateStore certificateStore, + @Nonnull OpenPgpFingerprint certificateFingerprint) + throws BadDataException, BadNameException, IOException { + String fingerprint = certificateFingerprint.toString().toLowerCase(); + Certificate certificateRecord = certificateStore.getCertificate(fingerprint); + if (certificateRecord == null) { + throw new NoSuchElementException("Cannot find certificate '" + certificateFingerprint + "'"); + } + PGPPublicKeyRing recipientCertificate = PGPainless.readKeyRing().publicKeyRing(certificateRecord.getInputStream()); + return addRecipient(recipientCertificate); + } + private void addRecipientKey(PGPPublicKeyRing keyRing, PGPPublicKey key) { encryptionKeys.add(new SubkeyIdentifier(keyRing, key.getKeyID())); PGPKeyEncryptionMethodGenerator encryptionMethod = ImplementationFactory From 6dc5b84d668388646085018ada1945e5933f346c Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Fri, 12 Aug 2022 15:05:42 +0200 Subject: [PATCH 104/763] Depend on pgp-certificate-store again --- .../encryption_signing/EncryptionOptions.java | 11 ++++++----- version.gradle | 2 +- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/EncryptionOptions.java b/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/EncryptionOptions.java index 4bf21434..13131f08 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/EncryptionOptions.java +++ b/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/EncryptionOptions.java @@ -32,8 +32,8 @@ import org.pgpainless.key.SubkeyIdentifier; import org.pgpainless.key.info.KeyAccessor; import org.pgpainless.key.info.KeyRingInfo; import org.pgpainless.util.Passphrase; -import pgp.certificate_store.Certificate; -import pgp.certificate_store.CertificateStore; +import pgp.certificate_store.PGPCertificateStore; +import pgp.certificate_store.certificate.Certificate; import pgp.certificate_store.exception.BadDataException; import pgp.certificate_store.exception.BadNameException; @@ -242,7 +242,7 @@ public class EncryptionOptions { } /** - * Add a recipient by providing a {@link CertificateStore} and the {@link OpenPgpFingerprint} of the recipients key. + * Add a recipient by providing a {@link PGPCertificateStore} and the {@link OpenPgpFingerprint} of the recipients key. * If no such certificate is found in the store, a {@link NoSuchElementException is thrown}. * * @param certificateStore certificate store @@ -253,7 +253,7 @@ public class EncryptionOptions { * @throws IOException in case of an IO error * @throws NoSuchElementException if the store does not contain a certificate for the given fingerprint */ - public EncryptionOptions addRecipient(@Nonnull CertificateStore certificateStore, + public EncryptionOptions addRecipient(@Nonnull PGPCertificateStore certificateStore, @Nonnull OpenPgpFingerprint certificateFingerprint) throws BadDataException, BadNameException, IOException { String fingerprint = certificateFingerprint.toString().toLowerCase(); @@ -261,7 +261,8 @@ public class EncryptionOptions { if (certificateRecord == null) { throw new NoSuchElementException("Cannot find certificate '" + certificateFingerprint + "'"); } - PGPPublicKeyRing recipientCertificate = PGPainless.readKeyRing().publicKeyRing(certificateRecord.getInputStream()); + PGPPublicKeyRing recipientCertificate = PGPainless.readKeyRing() + .publicKeyRing(certificateRecord.getInputStream()); return addRecipient(recipientCertificate); } diff --git a/version.gradle b/version.gradle index 302f54e0..260ef548 100644 --- a/version.gradle +++ b/version.gradle @@ -17,7 +17,7 @@ allprojects { junitVersion = '5.8.2' logbackVersion = '1.2.11' mockitoVersion = '4.5.1' - pgpCertificateStoreVersion = '0.1.1' + pgpCertificateStoreVersion = '0.1.2-SNAPSHOT' slf4jVersion = '1.7.36' sopJavaVersion = '4.0.7' } From d0277fbbecfeed576e1d0dab9960fe43deec368e Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 25 Aug 2022 17:20:45 +0200 Subject: [PATCH 105/763] Bump cert-d-java to 0.2.0 --- pgpainless-core/build.gradle | 3 ++- version.gradle | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/pgpainless-core/build.gradle b/pgpainless-core/build.gradle index 544fb8a4..84b4273f 100644 --- a/pgpainless-core/build.gradle +++ b/pgpainless-core/build.gradle @@ -25,7 +25,8 @@ dependencies { // api(files("../libs/bcpg-jdk18on-1.70.jar")) // certificate store - api "org.pgpainless:pgp-certificate-store:$pgpCertificateStoreVersion" + api "org.pgpainless:pgp-certificate-store:$pgpCertDJavaVersion" + testImplementation "org.pgpainless:pgpainless-cert-d:$pgpainlessCertDVersion" // @Nullable, @Nonnull annotations implementation "com.google.code.findbugs:jsr305:3.0.2" diff --git a/version.gradle b/version.gradle index 260ef548..109e592e 100644 --- a/version.gradle +++ b/version.gradle @@ -17,7 +17,8 @@ allprojects { junitVersion = '5.8.2' logbackVersion = '1.2.11' mockitoVersion = '4.5.1' - pgpCertificateStoreVersion = '0.1.2-SNAPSHOT' + pgpainlessCertDVersion = '0.1.3-SNAPSHOT' + pgpCertDJavaVersion = '0.2.0' slf4jVersion = '1.7.36' sopJavaVersion = '4.0.7' } From 22abb624435ec3fc5bd6ad974b6e0f5b2df75941 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 25 Aug 2022 18:56:37 +0200 Subject: [PATCH 106/763] Add test for encryption to cert from certificate store --- .../EncryptWithKeyFromKeyStoreTest.java | 111 ++++++++++++++++++ 1 file changed, 111 insertions(+) create mode 100644 pgpainless-core/src/test/java/org/pgpainless/encryption_signing/EncryptWithKeyFromKeyStoreTest.java diff --git a/pgpainless-core/src/test/java/org/pgpainless/encryption_signing/EncryptWithKeyFromKeyStoreTest.java b/pgpainless-core/src/test/java/org/pgpainless/encryption_signing/EncryptWithKeyFromKeyStoreTest.java new file mode 100644 index 00000000..9291d703 --- /dev/null +++ b/pgpainless-core/src/test/java/org/pgpainless/encryption_signing/EncryptWithKeyFromKeyStoreTest.java @@ -0,0 +1,111 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.encryption_signing; + +import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.PGPPublicKeyRing; +import org.bouncycastle.openpgp.PGPPublicKeyRingCollection; +import org.bouncycastle.util.io.Streams; +import org.junit.jupiter.api.Test; +import org.pgpainless.PGPainless; +import org.pgpainless.certificate_store.MergeCallbacks; +import org.pgpainless.certificate_store.PGPainlessCertD; +import org.pgpainless.key.OpenPgpFingerprint; +import pgp.cert_d.PGPCertificateStoreAdapter; +import pgp.certificate_store.certificate.Certificate; +import pgp.certificate_store.exception.BadDataException; +import pgp.certificate_store.exception.BadNameException; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class EncryptWithKeyFromKeyStoreTest { + + // Collection of 3 certificates (fingerprints below) + private static final String CERT_COLLECTION = "-----BEGIN PGP PUBLIC KEY BLOCK-----\n" + + "Version: BCPG v1.71\n" + + "\n" + + "mDMEYwemqhYJKwYBBAHaRw8BAQdAkhI29iXd05I2msAucVCmM2Chg52093a3MdHs\n" + + "NHu83i+0FEEgPGFAcGdwYWlubGVzcy5vcmc+iI8EExYKAEEFAmMHpqoJEAcHMNbM\n" + + "aq4mFiEEdjJKQcwZwKozs8H2Bwcw1sxqriYCngECmwEFFgIDAQAECwkIBwUVCgkI\n" + + "CwKZAQAAoZcBAPhMQ8TLPRMLiWcNi4bO3A9OonFpxOyfLRC8yiJSL6bbAQDcMylf\n" + + "pgOZGQ+uxWPokoJtWQt7I9IWsBxrwyW8iUniDbg4BGMHpqoSCisGAQQBl1UBBQEB\n" + + "B0D9g9P9VCLNFKteqgESsYK+OKeeDDk3LRgcsxANBkWNcwMBCAeIdQQYFgoAHQUC\n" + + "YwemqgKeAQKbDAUWAgMBAAQLCQgHBRUKCQgLAAoJEAcHMNbMaq4ml8MBAL8EUAIS\n" + + "cusSuhIJpep961GS7dvUjn/Hg0TW5llUzg8fAQCl3vSqbSsNAUxmrOr3600IuyM2\n" + + "EFlE412iEa5lhc/oArgzBGMHpqoWCSsGAQQB2kcPAQEHQKgUT/3nK/hqpgYR9zpw\n" + + "2AYOXsJSHRdJOGW6dEMRvIBaiNUEGBYKAH0FAmMHpqoCngECmwIFFgIDAQAECwkI\n" + + "BwUVCgkIC18gBBkWCgAGBQJjB6aqAAoJEGkhuqcOCjqNAfMA/3eG/POfM+6INiC3\n" + + "DY7mTMgSEqjojd3aBWAGdbGuQ0b+AQDgfhLFYN/Ip6dvbEAhf8d0TCrs4dFmS9Pp\n" + + "BOfWWgEsBgAKCRAHBzDWzGquJmIIAP4sQO7j7FH41KQf1E22SpbxiKSC2lK+9hxT\n" + + "kv6divqdZgD/T91FUb9AenAJBLzyaTwReQSt/iEx1mxLi7QilaJCDA6YMwRjB6aq\n" + + "FgkrBgEEAdpHDwEBB0D/8+N/bzvx2kZTrQ3fvGv6GTyBZGy1qqH9+70/orCegbQU\n" + + "QiA8YkBwZ3BhaW5sZXNzLm9yZz6IjwQTFgoAQQUCYwemqgkQDmT2NIT1D4IWIQTH\n" + + "zU3gfxyXLgTTOGAOZPY0hPUPggKeAQKbAQUWAgMBAAQLCQgHBRUKCQgLApkBAAAp\n" + + "3wEA4Pj8MpGKBiwG/I2A26B6IDz0MZ/IiR204tWjh54ZgIYA/RYrxyfdmuKhEzMf\n" + + "MA0a0juZ1euzxYPeNvgYJRjOnoUCuDgEYwemqhIKKwYBBAGXVQEFAQEHQPiGISsj\n" + + "Hv/wd8eQXUxMFU2I1ex6c9LcDXKOHHvL4XB1AwEIB4h1BBgWCgAdBQJjB6aqAp4B\n" + + "ApsMBRYCAwEABAsJCAcFFQoJCAsACgkQDmT2NIT1D4IdlQEA8cjOGf2X0D0v7gRg\n" + + "wV6C8o7KaIjwqbRFjbw4v7dewT4BANjBU/KD+SgKG9l27t0pv7fzklWxUwehfIYR\n" + + "veVegsEKuDMEYwemqhYJKwYBBAHaRw8BAQdAZA6ryWxnMEfhxhmfA8n54fgQXUE2\n" + + "BwPobUGLfhjee8eI1QQYFgoAfQUCYwemqgKeAQKbAgUWAgMBAAQLCQgHBRUKCQgL\n" + + "XyAEGRYKAAYFAmMHpqoACgkQB/5eyqSzV2hurwEAv0ODS93BTlgBXL6dDZ+6vO+y\n" + + "emW6wH4RBZcrvQOhmpMBALhVrbS6L97HukL9B9QMSyP9Ir3QrBJihJNQjIcs/9UD\n" + + "AAoJEA5k9jSE9Q+CAogA/jdAcbky2S6Ym39EqV8xCQKr7ddOKMzMUhGM65W6sAlP\n" + + "AP99OZu3bNXsH79eJ8KmSpTaRH8meRgSaIve/6NgAmO2CpgzBGMHpqoWCSsGAQQB\n" + + "2kcPAQEHQOdxRopw4vC2USLv4kqEVKNlAM+NkYomruNqsVlde9iutBRDIDxjQHBn\n" + + "cGFpbmxlc3Mub3JnPoiPBBMWCgBBBQJjB6aqCRBUXcAjBoWYJBYhBJFl6D4X+Xm9\n" + + "JjH+/VRdwCMGhZgkAp4BApsBBRYCAwEABAsJCAcFFQoJCAsCmQEAAAD4AQD9PvbC\n" + + "y/SNXx62jnQmNHVXo/UDOmUqHymwHvm0MrKHeQEA06X5eLoHsttbRTvQt4NVYjdy\n" + + "pDT4ySNvQCu6a5CiKg+4OARjB6aqEgorBgEEAZdVAQUBAQdAV1TW1qj0O2DGGBnR\n" + + "y11gSj6uHhxOaGps2QE9asfp7QEDAQgHiHUEGBYKAB0FAmMHpqoCngECmwwFFgID\n" + + "AQAECwkIBwUVCgkICwAKCRBUXcAjBoWYJIY9AQCVPseDfgRuCG7ygCmPtLO3Vp5j\n" + + "ZcDF1fke/J3Z6LVAvQEA2bxaKgArPRrTlmCgM7iJSOBVyzryWZ7+lmbjLeqVxgi4\n" + + "MwRjB6aqFgkrBgEEAdpHDwEBB0CCEWkyuz0HoWS63dinLk2VZJde4s4w0sQR9pPB\n" + + "6wlwvIjVBBgWCgB9BQJjB6aqAp4BApsCBRYCAwEABAsJCAcFFQoJCAtfIAQZFgoA\n" + + "BgUCYwemqgAKCRD9CgygdUb5mXWoAP0Zp5qSRFMJEghCgnZqcGIjlotGUc65uXv4\n" + + "U5iqHfgEJAD/aAhA55MmlxIDXUkDMlKsy8WfhksLu6dfMkjJY2LYEAgACgkQVF3A\n" + + "IwaFmCTV5wD9ErsC4w6ajM6PTGImtrK6IJEdMGajOwSGYWHiX9yaOI4BANdWby+h\n" + + "Pr2snaTp6/NukIbg3D/YMXm+mM6119v7HJkO\n" + + "=KVYs\n" + + "-----END PGP PUBLIC KEY BLOCK-----"; + private static final OpenPgpFingerprint cert1fp = OpenPgpFingerprint.parse("76324A41CC19C0AA33B3C1F6070730D6CC6AAE26"); + private static final OpenPgpFingerprint cert2fp = OpenPgpFingerprint.parse("C7CD4DE07F1C972E04D338600E64F63484F50F82"); + private static final OpenPgpFingerprint cert3fp = OpenPgpFingerprint.parse("9165E83E17F979BD2631FEFD545DC02306859824"); + + @Test + public void encryptWithKeyFromStore() throws PGPException, IOException, BadDataException, InterruptedException, BadNameException { + // In-Memory certificate store + PGPainlessCertD certificateDirectory = PGPainlessCertD.inMemory(); + PGPCertificateStoreAdapter adapter = new PGPCertificateStoreAdapter(certificateDirectory); + + // Populate store + PGPPublicKeyRingCollection certificates = PGPainless.readKeyRing().publicKeyRingCollection(CERT_COLLECTION); + for (PGPPublicKeyRing cert : certificates) { + certificateDirectory.insert(new ByteArrayInputStream(cert.getEncoded()), MergeCallbacks.mergeWithExisting()); + } + + // Encrypt message + ByteArrayOutputStream ciphertextOut = new ByteArrayOutputStream(); + EncryptionStream encryptionStream = PGPainless.encryptAndOrSign() + .onOutputStream(ciphertextOut) + .withOptions(ProducerOptions.encrypt( + EncryptionOptions.encryptCommunications() + .addRecipient(adapter, cert2fp))); + ByteArrayInputStream plaintext = new ByteArrayInputStream("Hello, World! This message is encrypted using a cert from a store!".getBytes()); + Streams.pipeAll(plaintext, encryptionStream); + encryptionStream.close(); + + // Get cert from store + Certificate cert = adapter.getCertificate(cert2fp.toString()); + PGPPublicKeyRing publicKeys = PGPainless.readKeyRing().publicKeyRing(cert.getInputStream()); + + // check if message was encrypted for cert + assertTrue(encryptionStream.getResult().isEncryptedFor(publicKeys)); + } +} From 4594b494a96089534f597fb2a6b14ca292ef8959 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 25 Aug 2022 19:46:28 +0200 Subject: [PATCH 107/763] Implement signature verification with certificate stores as cert source --- .../ConsumerOptions.java | 93 +++++++- .../EncryptWithKeyFromKeyStoreTest.java | 213 ++++++++++++++---- .../pgpainless/sop/DetachedVerifyImpl.java | 2 +- .../org/pgpainless/sop/InlineVerifyImpl.java | 2 +- 4 files changed, 257 insertions(+), 53 deletions(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/ConsumerOptions.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/ConsumerOptions.java index f8f59d36..f7b3c020 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/ConsumerOptions.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/ConsumerOptions.java @@ -6,10 +6,12 @@ package org.pgpainless.decryption_verification; import java.io.IOException; import java.io.InputStream; +import java.util.ArrayList; import java.util.Collections; import java.util.Date; import java.util.HashMap; import java.util.HashSet; +import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; @@ -23,6 +25,7 @@ import org.bouncycastle.openpgp.PGPSecretKeyRing; import org.bouncycastle.openpgp.PGPSecretKeyRingCollection; import org.bouncycastle.openpgp.PGPSignature; import org.bouncycastle.openpgp.operator.PublicKeyDataDecryptorFactory; +import org.pgpainless.PGPainless; import org.pgpainless.decryption_verification.cleartext_signatures.InMemoryMultiPassStrategy; import org.pgpainless.decryption_verification.cleartext_signatures.MultiPassStrategy; import org.pgpainless.key.SubkeyIdentifier; @@ -30,6 +33,9 @@ import org.pgpainless.key.protection.SecretKeyRingProtector; import org.pgpainless.signature.SignatureUtils; import org.pgpainless.util.Passphrase; import org.pgpainless.util.SessionKey; +import pgp.certificate_store.PGPCertificateStore; +import pgp.certificate_store.certificate.Certificate; +import pgp.certificate_store.exception.BadDataException; /** * Options for decryption and signature verification. @@ -43,8 +49,7 @@ public class ConsumerOptions { private Date verifyNotBefore = null; private Date verifyNotAfter = new Date(); - // Set of verification keys - private final Set certificates = new HashSet<>(); + private final CertificateSource certificates = new CertificateSource(); private final Set detachedSignatures = new HashSet<>(); private MissingPublicKeyCallback missingCertificateCallback = null; @@ -113,7 +118,7 @@ public class ConsumerOptions { * @return options */ public ConsumerOptions addVerificationCert(PGPPublicKeyRing verificationCert) { - this.certificates.add(verificationCert); + this.certificates.addCertificate(verificationCert); return this; } @@ -130,6 +135,11 @@ public class ConsumerOptions { return this; } + public ConsumerOptions addVerificationCerts(PGPCertificateStore certificateStore) { + this.certificates.addStore(certificateStore); + return this; + } + public ConsumerOptions addVerificationOfDetachedSignatures(InputStream signatureInputStream) throws IOException, PGPException { List signatures = SignatureUtils.readSignatures(signatureInputStream); return addVerificationOfDetachedSignatures(signatures); @@ -266,8 +276,19 @@ public class ConsumerOptions { return Collections.unmodifiableSet(decryptionPassphrases); } + /** + * Return the explicitly set verification certificates. + * + * @deprecated use {@link #getCertificateSource()} instead. + * @return verification certs + */ + @Deprecated public @Nonnull Set getCertificates() { - return Collections.unmodifiableSet(certificates); + return certificates.getExplicitCertificates(); + } + + public @Nonnull CertificateSource getCertificateSource() { + return certificates; } public @Nullable MissingPublicKeyCallback getMissingCertificateCallback() { @@ -385,4 +406,68 @@ public class ConsumerOptions { public MultiPassStrategy getMultiPassStrategy() { return multiPassStrategy; } + + public static class CertificateSource { + + private List stores = new ArrayList<>(); + private Set explicitCertificates = new HashSet<>(); + + /** + * Add a certificate store as source for verification certificates. + * + * @param certificateStore cert store + */ + public void addStore(PGPCertificateStore certificateStore) { + this.stores.add(certificateStore); + } + + /** + * Add a certificate as verification cert explicitly. + * + * @param certificate certificate + */ + public void addCertificate(PGPPublicKeyRing certificate) { + this.explicitCertificates.add(certificate); + } + + /** + * Return the set of explicitly set verification certificates. + * @return explicitly set verification certs + */ + public Set getExplicitCertificates() { + return Collections.unmodifiableSet(explicitCertificates); + } + + /** + * Return a certificate which contains a subkey with the given keyId. + * This method first checks all explicitly set verification certs and if no cert is found it consults + * the certificate stores. + * + * @param keyId key id + * @return certificate + */ + public PGPPublicKeyRing getCertificate(long keyId) { + + for (PGPPublicKeyRing cert : explicitCertificates) { + if (cert.getPublicKey(keyId) != null) { + return cert; + } + } + + for (PGPCertificateStore store : stores) { + try { + Iterator certs = store.getCertificatesBySubkeyId(keyId); + if (!certs.hasNext()) { + continue; + } + Certificate cert = certs.next(); + PGPPublicKeyRing publicKey = PGPainless.readKeyRing().publicKeyRing(cert.getInputStream()); + return publicKey; + } catch (IOException | BadDataException e) { + continue; + } + } + return null; + } + } } diff --git a/pgpainless-core/src/test/java/org/pgpainless/encryption_signing/EncryptWithKeyFromKeyStoreTest.java b/pgpainless-core/src/test/java/org/pgpainless/encryption_signing/EncryptWithKeyFromKeyStoreTest.java index 9291d703..48f1bbd7 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/encryption_signing/EncryptWithKeyFromKeyStoreTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/encryption_signing/EncryptWithKeyFromKeyStoreTest.java @@ -7,12 +7,18 @@ package org.pgpainless.encryption_signing; import org.bouncycastle.openpgp.PGPException; import org.bouncycastle.openpgp.PGPPublicKeyRing; import org.bouncycastle.openpgp.PGPPublicKeyRingCollection; +import org.bouncycastle.openpgp.PGPSecretKeyRing; +import org.bouncycastle.openpgp.PGPSecretKeyRingCollection; import org.bouncycastle.util.io.Streams; import org.junit.jupiter.api.Test; import org.pgpainless.PGPainless; import org.pgpainless.certificate_store.MergeCallbacks; import org.pgpainless.certificate_store.PGPainlessCertD; +import org.pgpainless.decryption_verification.ConsumerOptions; +import org.pgpainless.decryption_verification.DecryptionStream; +import org.pgpainless.decryption_verification.OpenPgpMetadata; import org.pgpainless.key.OpenPgpFingerprint; +import org.pgpainless.key.protection.SecretKeyRingProtector; import pgp.cert_d.PGPCertificateStoreAdapter; import pgp.certificate_store.certificate.Certificate; import pgp.certificate_store.exception.BadDataException; @@ -26,60 +32,116 @@ import static org.junit.jupiter.api.Assertions.assertTrue; public class EncryptWithKeyFromKeyStoreTest { + // Collection of 3 keys (fingerprints below) + private static final String KEY_COLLECTION = "-----BEGIN PGP PRIVATE KEY BLOCK-----\n" + + "Version: BCPG v1.71\n" + + "\n" + + "lFgEYwerQBYJKwYBBAHaRw8BAQdAl3XjFMXQdmhMuFEIbE7IJUP1k+5utUT6IAW3\n" + + "zlWguvQAAQDK7Qh5Q9EAB5cTh2OWsPeydfDqRmnuxlZjlwf4WWQLhRAltBRBIDxh\n" + + "QHBncGFpbmxlc3Mub3JnPoiPBBMWCgBBBQJjB6tBCRBoj2Vso6FpsxYhBNqK9ZX8\n" + + "QfcbxPJmCGiPZWyjoWmzAp4BApsBBRYCAwEABAsJCAcFFQoJCAsCmQEAACEaAP9P\n" + + "49Q/E19vyx2rV8EjQd+XBFnDuYxBjw80ZVC0TaKJNgEAgWsQqcg/ARkG9XGxaE3X\n" + + "IE9tFHh4wpjQhnK1Ta/wJAOcXQRjB6tBEgorBgEEAZdVAQUBAQdATJM1XKfKVF+C\n" + + "B2/xrGU+F89Ir9viOut4sna4aWfvwHoDAQgHAAD/UN84yv5jxKsPgfw/XZCDwoey\n" + + "Y69ompSiBuZjzOWrjegToIh1BBgWCgAdBQJjB6tBAp4BApsMBRYCAwEABAsJCAcF\n" + + "FQoJCAsACgkQaI9lbKOhabP/PAEApov4hYuhIENq26z+w4s3A1gakN+gax54F7+M\n" + + "YSUm16sBAPiuEdpVJOwTk3WMXKyLOYaVU3JstlP2H1ouguvYTt4CnFgEYwerQRYJ\n" + + "KwYBBAHaRw8BAQdA5xpeGHNy9v+QUbl+Rs7Mx0c6D913gksW1eZ4Qeg31B0AAQCx\n" + + "6b3P5lRBAraZstlRupymrt6vF2JpeJB8JOOQ+rdVYBJpiNUEGBYKAH0FAmMHq0EC\n" + + "ngECmwIFFgIDAQAECwkIBwUVCgkIC18gBBkWCgAGBQJjB6tBAAoJENH9GnI3A/RM\n" + + "IVMA/1GU9E+vA8bs0vJVDjp1ri3J4S7u+abwmlivDw8g8XCWAPwKWWfHLgJCsAHk\n" + + "INuDgJdqbNPATFiXxH9FqYnOvWy6DAAKCRBoj2Vso6Fps884AP9D5ZOwuBEXyT/j\n" + + "0G8CWBZ0lT14kRGFucjQi9kZStAuVgEA5cd3eUWofnekd/P6R3UgmvhVOqvxwUUg\n" + + "Y3mEArH7+waUWARjB6tBFgkrBgEEAdpHDwEBB0BCYWjTs0pfBnKYgO0O07djiMSB\n" + + "tUJVpUFo6zrVK92RgAAA/38G6IEK5rJs1OCusmmhHJk1vDu0hbesK7JH7dh75mVY\n" + + "Ep20FEIgPGJAcGdwYWlubGVzcy5vcmc+iI8EExYKAEEFAmMHq0EJEAnsE6FTTHNl\n" + + "FiEE2/L5HBba6IFDHu8cCewToVNMc2UCngECmwEFFgIDAQAECwkIBwUVCgkICwKZ\n" + + "AQAAS7MBAI74uYLK7XR6oCwWYk7C6nwdgu3t478MaEpVHQz/9nEGAQCvJCYqqOd6\n" + + "cAG6fwFaIJ3h99/Y5o2NaiN17S2zOXEZDJxdBGMHq0ESCisGAQQBl1UBBQEBB0BU\n" + + "EjXQCT4xwJryksXsMLaFo43pFTwWaTzduiWgCy2KMgMBCAcAAP9lXlnMYtBfXpgH\n" + + "doUZZk3cvWBOH3awc12V3jZSLtSE8BAJiHUEGBYKAB0FAmMHq0ECngECmwwFFgID\n" + + "AQAECwkIBwUVCgkICwAKCRAJ7BOhU0xzZf5lAQDOgzMhqg3fE8Hg4Hbt4+B0fAD0\n" + + "kp6EJgsKRWT7KbZ0SQD/aVGFv7VRVqiiqOT/YMQKBBwHnq/CGJqxUwUmavBMRAqc\n" + + "WARjB6tBFgkrBgEEAdpHDwEBB0A5kv3bpsnlxs2LrAzeBx4RgtXQNBhGRhzko1to\n" + + "4q+ebQAA/1SU1hvrqd9gNmcc4wff1iwJ1dnqnrbGbO1Yz9rYZjXRE4iI1QQYFgoA\n" + + "fQUCYwerQQKeAQKbAgUWAgMBAAQLCQgHBRUKCQgLXyAEGRYKAAYFAmMHq0EACgkQ\n" + + "pYWdiAVpxGRW4AD+Lade9kJrvcBMSq8EERhYTH6DFka4eMgFB76kH31WmpQA+gOU\n" + + "7kwqKmtyVsXVgCLGMcdTvbZr+73C5m8R7LsdY5kEAAoJEAnsE6FTTHNl7BAA/2v8\n" + + "Wzfmg1OO6IWCohmmNgF4rIDBW8Q9s3+1I/mWlMyjAP9YGR+fnN/YOQrlSG9UiXE5\n" + + "fGwUhaPB0LEGWp0wmmQYA5RYBGMHq0EWCSsGAQQB2kcPAQEHQI8C53+C8crLCQ48\n" + + "OKQa1dEKc8XWQSA6Ckg5j73tOJRLAAD/VRvioGU2M9G6+eKTn68mBVZ8G512HELr\n" + + "apK9M5UFGUMPXLQUQyA8Y0BwZ3BhaW5sZXNzLm9yZz6IjwQTFgoAQQUCYwerQQkQ\n" + + "ommXHYx1l94WIQQp+Mrw86EV1myUgUKiaZcdjHWX3gKeAQKbAQUWAgMBAAQLCQgH\n" + + "BRUKCQgLApkBAAAQ5wEAvahnnRuwY+Y7EPSQG+sqhsdvSTumleYPtEOnHfKctpkA\n" + + "/iaTp4OoUw/RtyWUAk8MLN47CAW5wwhFUbVfZOaS88wMnF0EYwerQRIKKwYBBAGX\n" + + "VQEFAQEHQNz/s68ZGUBfDmMz510cFgHz+mAdC2nXeE4hHKV/HIVsAwEIBwAA/1HB\n" + + "vRl84B8r/PY+5j/X6A+4J08QB/vd5wIHVdkrX+xQELGIdQQYFgoAHQUCYwerQQKe\n" + + "AQKbDAUWAgMBAAQLCQgHBRUKCQgLAAoJEKJplx2MdZfeqzYA/jLtjRmy42MCOxnF\n" + + "3A95WZIDoEohFU0QAeE/yVTLGoDTAP4xhTznleABK7VbD9GJXfD6DkEC749tOsST\n" + + "eYO/GOxKDpxYBGMHq0EWCSsGAQQB2kcPAQEHQFnvyWSgOv4gn3Ch3RY74pRg+7hX\n" + + "OBJAf6ybwvx9t4olAAEAwYG1CL0JozVD1216yrENkP8La132O1MI28kqMsoF6FcP\n" + + "I4jVBBgWCgB9BQJjB6tBAp4BApsCBRYCAwEABAsJCAcFFQoJCAtfIAQZFgoABgUC\n" + + "YwerQQAKCRB8jJGVps/ENgz7AP9ZMENJH+rIKMjynb9WPBlvJ8yJ9dMhzCxcssxg\n" + + "EVZYXAEA5ZsE5xJLQC/cVMGFvqaQ8iPo5jhDZpQJ8RCVlb8XzQwACgkQommXHYx1\n" + + "l96SkgD/f0FYkK4yB8FWuntJ3n0FUfE31wDwpxvvpvP+o3d2GB4BAP9LRKBXMwj4\n" + + "jzJc4ViKmwiNJAPttDQCpYjzJT7LUKAA\n" + + "=EAvh\n" + + "-----END PGP PRIVATE KEY BLOCK-----"; + // Collection of 3 certificates (fingerprints below) private static final String CERT_COLLECTION = "-----BEGIN PGP PUBLIC KEY BLOCK-----\n" + "Version: BCPG v1.71\n" + "\n" + - "mDMEYwemqhYJKwYBBAHaRw8BAQdAkhI29iXd05I2msAucVCmM2Chg52093a3MdHs\n" + - "NHu83i+0FEEgPGFAcGdwYWlubGVzcy5vcmc+iI8EExYKAEEFAmMHpqoJEAcHMNbM\n" + - "aq4mFiEEdjJKQcwZwKozs8H2Bwcw1sxqriYCngECmwEFFgIDAQAECwkIBwUVCgkI\n" + - "CwKZAQAAoZcBAPhMQ8TLPRMLiWcNi4bO3A9OonFpxOyfLRC8yiJSL6bbAQDcMylf\n" + - "pgOZGQ+uxWPokoJtWQt7I9IWsBxrwyW8iUniDbg4BGMHpqoSCisGAQQBl1UBBQEB\n" + - "B0D9g9P9VCLNFKteqgESsYK+OKeeDDk3LRgcsxANBkWNcwMBCAeIdQQYFgoAHQUC\n" + - "YwemqgKeAQKbDAUWAgMBAAQLCQgHBRUKCQgLAAoJEAcHMNbMaq4ml8MBAL8EUAIS\n" + - "cusSuhIJpep961GS7dvUjn/Hg0TW5llUzg8fAQCl3vSqbSsNAUxmrOr3600IuyM2\n" + - "EFlE412iEa5lhc/oArgzBGMHpqoWCSsGAQQB2kcPAQEHQKgUT/3nK/hqpgYR9zpw\n" + - "2AYOXsJSHRdJOGW6dEMRvIBaiNUEGBYKAH0FAmMHpqoCngECmwIFFgIDAQAECwkI\n" + - "BwUVCgkIC18gBBkWCgAGBQJjB6aqAAoJEGkhuqcOCjqNAfMA/3eG/POfM+6INiC3\n" + - "DY7mTMgSEqjojd3aBWAGdbGuQ0b+AQDgfhLFYN/Ip6dvbEAhf8d0TCrs4dFmS9Pp\n" + - "BOfWWgEsBgAKCRAHBzDWzGquJmIIAP4sQO7j7FH41KQf1E22SpbxiKSC2lK+9hxT\n" + - "kv6divqdZgD/T91FUb9AenAJBLzyaTwReQSt/iEx1mxLi7QilaJCDA6YMwRjB6aq\n" + - "FgkrBgEEAdpHDwEBB0D/8+N/bzvx2kZTrQ3fvGv6GTyBZGy1qqH9+70/orCegbQU\n" + - "QiA8YkBwZ3BhaW5sZXNzLm9yZz6IjwQTFgoAQQUCYwemqgkQDmT2NIT1D4IWIQTH\n" + - "zU3gfxyXLgTTOGAOZPY0hPUPggKeAQKbAQUWAgMBAAQLCQgHBRUKCQgLApkBAAAp\n" + - "3wEA4Pj8MpGKBiwG/I2A26B6IDz0MZ/IiR204tWjh54ZgIYA/RYrxyfdmuKhEzMf\n" + - "MA0a0juZ1euzxYPeNvgYJRjOnoUCuDgEYwemqhIKKwYBBAGXVQEFAQEHQPiGISsj\n" + - "Hv/wd8eQXUxMFU2I1ex6c9LcDXKOHHvL4XB1AwEIB4h1BBgWCgAdBQJjB6aqAp4B\n" + - "ApsMBRYCAwEABAsJCAcFFQoJCAsACgkQDmT2NIT1D4IdlQEA8cjOGf2X0D0v7gRg\n" + - "wV6C8o7KaIjwqbRFjbw4v7dewT4BANjBU/KD+SgKG9l27t0pv7fzklWxUwehfIYR\n" + - "veVegsEKuDMEYwemqhYJKwYBBAHaRw8BAQdAZA6ryWxnMEfhxhmfA8n54fgQXUE2\n" + - "BwPobUGLfhjee8eI1QQYFgoAfQUCYwemqgKeAQKbAgUWAgMBAAQLCQgHBRUKCQgL\n" + - "XyAEGRYKAAYFAmMHpqoACgkQB/5eyqSzV2hurwEAv0ODS93BTlgBXL6dDZ+6vO+y\n" + - "emW6wH4RBZcrvQOhmpMBALhVrbS6L97HukL9B9QMSyP9Ir3QrBJihJNQjIcs/9UD\n" + - "AAoJEA5k9jSE9Q+CAogA/jdAcbky2S6Ym39EqV8xCQKr7ddOKMzMUhGM65W6sAlP\n" + - "AP99OZu3bNXsH79eJ8KmSpTaRH8meRgSaIve/6NgAmO2CpgzBGMHpqoWCSsGAQQB\n" + - "2kcPAQEHQOdxRopw4vC2USLv4kqEVKNlAM+NkYomruNqsVlde9iutBRDIDxjQHBn\n" + - "cGFpbmxlc3Mub3JnPoiPBBMWCgBBBQJjB6aqCRBUXcAjBoWYJBYhBJFl6D4X+Xm9\n" + - "JjH+/VRdwCMGhZgkAp4BApsBBRYCAwEABAsJCAcFFQoJCAsCmQEAAAD4AQD9PvbC\n" + - "y/SNXx62jnQmNHVXo/UDOmUqHymwHvm0MrKHeQEA06X5eLoHsttbRTvQt4NVYjdy\n" + - "pDT4ySNvQCu6a5CiKg+4OARjB6aqEgorBgEEAZdVAQUBAQdAV1TW1qj0O2DGGBnR\n" + - "y11gSj6uHhxOaGps2QE9asfp7QEDAQgHiHUEGBYKAB0FAmMHpqoCngECmwwFFgID\n" + - "AQAECwkIBwUVCgkICwAKCRBUXcAjBoWYJIY9AQCVPseDfgRuCG7ygCmPtLO3Vp5j\n" + - "ZcDF1fke/J3Z6LVAvQEA2bxaKgArPRrTlmCgM7iJSOBVyzryWZ7+lmbjLeqVxgi4\n" + - "MwRjB6aqFgkrBgEEAdpHDwEBB0CCEWkyuz0HoWS63dinLk2VZJde4s4w0sQR9pPB\n" + - "6wlwvIjVBBgWCgB9BQJjB6aqAp4BApsCBRYCAwEABAsJCAcFFQoJCAtfIAQZFgoA\n" + - "BgUCYwemqgAKCRD9CgygdUb5mXWoAP0Zp5qSRFMJEghCgnZqcGIjlotGUc65uXv4\n" + - "U5iqHfgEJAD/aAhA55MmlxIDXUkDMlKsy8WfhksLu6dfMkjJY2LYEAgACgkQVF3A\n" + - "IwaFmCTV5wD9ErsC4w6ajM6PTGImtrK6IJEdMGajOwSGYWHiX9yaOI4BANdWby+h\n" + - "Pr2snaTp6/NukIbg3D/YMXm+mM6119v7HJkO\n" + - "=KVYs\n" + + "mDMEYwerQBYJKwYBBAHaRw8BAQdAl3XjFMXQdmhMuFEIbE7IJUP1k+5utUT6IAW3\n" + + "zlWguvS0FEEgPGFAcGdwYWlubGVzcy5vcmc+iI8EExYKAEEFAmMHq0EJEGiPZWyj\n" + + "oWmzFiEE2or1lfxB9xvE8mYIaI9lbKOhabMCngECmwEFFgIDAQAECwkIBwUVCgkI\n" + + "CwKZAQAAIRoA/0/j1D8TX2/LHatXwSNB35cEWcO5jEGPDzRlULRNook2AQCBaxCp\n" + + "yD8BGQb1cbFoTdcgT20UeHjCmNCGcrVNr/AkA7g4BGMHq0ESCisGAQQBl1UBBQEB\n" + + "B0BMkzVcp8pUX4IHb/GsZT4Xz0iv2+I663iydrhpZ+/AegMBCAeIdQQYFgoAHQUC\n" + + "YwerQQKeAQKbDAUWAgMBAAQLCQgHBRUKCQgLAAoJEGiPZWyjoWmz/zwBAKaL+IWL\n" + + "oSBDatus/sOLNwNYGpDfoGseeBe/jGElJterAQD4rhHaVSTsE5N1jFysizmGlVNy\n" + + "bLZT9h9aLoLr2E7eArgzBGMHq0EWCSsGAQQB2kcPAQEHQOcaXhhzcvb/kFG5fkbO\n" + + "zMdHOg/dd4JLFtXmeEHoN9QdiNUEGBYKAH0FAmMHq0ECngECmwIFFgIDAQAECwkI\n" + + "BwUVCgkIC18gBBkWCgAGBQJjB6tBAAoJENH9GnI3A/RMIVMA/1GU9E+vA8bs0vJV\n" + + "Djp1ri3J4S7u+abwmlivDw8g8XCWAPwKWWfHLgJCsAHkINuDgJdqbNPATFiXxH9F\n" + + "qYnOvWy6DAAKCRBoj2Vso6Fps884AP9D5ZOwuBEXyT/j0G8CWBZ0lT14kRGFucjQ\n" + + "i9kZStAuVgEA5cd3eUWofnekd/P6R3UgmvhVOqvxwUUgY3mEArH7+waYMwRjB6tB\n" + + "FgkrBgEEAdpHDwEBB0BCYWjTs0pfBnKYgO0O07djiMSBtUJVpUFo6zrVK92RgLQU\n" + + "QiA8YkBwZ3BhaW5sZXNzLm9yZz6IjwQTFgoAQQUCYwerQQkQCewToVNMc2UWIQTb\n" + + "8vkcFtrogUMe7xwJ7BOhU0xzZQKeAQKbAQUWAgMBAAQLCQgHBRUKCQgLApkBAABL\n" + + "swEAjvi5gsrtdHqgLBZiTsLqfB2C7e3jvwxoSlUdDP/2cQYBAK8kJiqo53pwAbp/\n" + + "AVogneH339jmjY1qI3XtLbM5cRkMuDgEYwerQRIKKwYBBAGXVQEFAQEHQFQSNdAJ\n" + + "PjHAmvKSxewwtoWjjekVPBZpPN26JaALLYoyAwEIB4h1BBgWCgAdBQJjB6tBAp4B\n" + + "ApsMBRYCAwEABAsJCAcFFQoJCAsACgkQCewToVNMc2X+ZQEAzoMzIaoN3xPB4OB2\n" + + "7ePgdHwA9JKehCYLCkVk+ym2dEkA/2lRhb+1UVaooqjk/2DECgQcB56vwhiasVMF\n" + + "JmrwTEQKuDMEYwerQRYJKwYBBAHaRw8BAQdAOZL926bJ5cbNi6wM3gceEYLV0DQY\n" + + "RkYc5KNbaOKvnm2I1QQYFgoAfQUCYwerQQKeAQKbAgUWAgMBAAQLCQgHBRUKCQgL\n" + + "XyAEGRYKAAYFAmMHq0EACgkQpYWdiAVpxGRW4AD+Lade9kJrvcBMSq8EERhYTH6D\n" + + "Fka4eMgFB76kH31WmpQA+gOU7kwqKmtyVsXVgCLGMcdTvbZr+73C5m8R7LsdY5kE\n" + + "AAoJEAnsE6FTTHNl7BAA/2v8Wzfmg1OO6IWCohmmNgF4rIDBW8Q9s3+1I/mWlMyj\n" + + "AP9YGR+fnN/YOQrlSG9UiXE5fGwUhaPB0LEGWp0wmmQYA5gzBGMHq0EWCSsGAQQB\n" + + "2kcPAQEHQI8C53+C8crLCQ48OKQa1dEKc8XWQSA6Ckg5j73tOJRLtBRDIDxjQHBn\n" + + "cGFpbmxlc3Mub3JnPoiPBBMWCgBBBQJjB6tBCRCiaZcdjHWX3hYhBCn4yvDzoRXW\n" + + "bJSBQqJplx2MdZfeAp4BApsBBRYCAwEABAsJCAcFFQoJCAsCmQEAABDnAQC9qGed\n" + + "G7Bj5jsQ9JAb6yqGx29JO6aV5g+0Q6cd8py2mQD+JpOng6hTD9G3JZQCTwws3jsI\n" + + "BbnDCEVRtV9k5pLzzAy4OARjB6tBEgorBgEEAZdVAQUBAQdA3P+zrxkZQF8OYzPn\n" + + "XRwWAfP6YB0Ladd4TiEcpX8chWwDAQgHiHUEGBYKAB0FAmMHq0ECngECmwwFFgID\n" + + "AQAECwkIBwUVCgkICwAKCRCiaZcdjHWX3qs2AP4y7Y0ZsuNjAjsZxdwPeVmSA6BK\n" + + "IRVNEAHhP8lUyxqA0wD+MYU855XgASu1Ww/RiV3w+g5BAu+PbTrEk3mDvxjsSg64\n" + + "MwRjB6tBFgkrBgEEAdpHDwEBB0BZ78lkoDr+IJ9wod0WO+KUYPu4VzgSQH+sm8L8\n" + + "fbeKJYjVBBgWCgB9BQJjB6tBAp4BApsCBRYCAwEABAsJCAcFFQoJCAtfIAQZFgoA\n" + + "BgUCYwerQQAKCRB8jJGVps/ENgz7AP9ZMENJH+rIKMjynb9WPBlvJ8yJ9dMhzCxc\n" + + "ssxgEVZYXAEA5ZsE5xJLQC/cVMGFvqaQ8iPo5jhDZpQJ8RCVlb8XzQwACgkQommX\n" + + "HYx1l96SkgD/f0FYkK4yB8FWuntJ3n0FUfE31wDwpxvvpvP+o3d2GB4BAP9LRKBX\n" + + "Mwj4jzJc4ViKmwiNJAPttDQCpYjzJT7LUKAA\n" + + "=WaRm\n" + "-----END PGP PUBLIC KEY BLOCK-----"; - private static final OpenPgpFingerprint cert1fp = OpenPgpFingerprint.parse("76324A41CC19C0AA33B3C1F6070730D6CC6AAE26"); - private static final OpenPgpFingerprint cert2fp = OpenPgpFingerprint.parse("C7CD4DE07F1C972E04D338600E64F63484F50F82"); - private static final OpenPgpFingerprint cert3fp = OpenPgpFingerprint.parse("9165E83E17F979BD2631FEFD545DC02306859824"); + private static final OpenPgpFingerprint cert1fp = OpenPgpFingerprint.parse("DA8AF595FC41F71BC4F26608688F656CA3A169B3"); + private static final OpenPgpFingerprint cert2fp = OpenPgpFingerprint.parse("DBF2F91C16DAE881431EEF1C09EC13A1534C7365"); + private static final OpenPgpFingerprint cert3fp = OpenPgpFingerprint.parse("29F8CAF0F3A115D66C948142A269971D8C7597DE"); @Test - public void encryptWithKeyFromStore() throws PGPException, IOException, BadDataException, InterruptedException, BadNameException { + public void encryptWithCertFromCertificateStore() throws PGPException, IOException, BadDataException, InterruptedException, BadNameException { // In-Memory certificate store PGPainlessCertD certificateDirectory = PGPainlessCertD.inMemory(); PGPCertificateStoreAdapter adapter = new PGPCertificateStoreAdapter(certificateDirectory); @@ -108,4 +170,61 @@ public class EncryptWithKeyFromKeyStoreTest { // check if message was encrypted for cert assertTrue(encryptionStream.getResult().isEncryptedFor(publicKeys)); } + + @Test + public void verifyWithCertFromCertificateStore() + throws PGPException, IOException, BadDataException, InterruptedException, BadNameException { + // In-Memory certificate store + PGPainlessCertD certificateDirectory = PGPainlessCertD.inMemory(); + PGPCertificateStoreAdapter adapter = new PGPCertificateStoreAdapter(certificateDirectory); + + // Populate store + PGPPublicKeyRingCollection certificates = PGPainless.readKeyRing().publicKeyRingCollection(CERT_COLLECTION); + for (PGPPublicKeyRing cert : certificates) { + certificateDirectory.insert(new ByteArrayInputStream(cert.getEncoded()), MergeCallbacks.mergeWithExisting()); + } + + // Prepare keys + OpenPgpFingerprint cryptFp = cert3fp; + OpenPgpFingerprint signFp = cert1fp; + PGPSecretKeyRingCollection secretKeys = PGPainless.readKeyRing().secretKeyRingCollection(KEY_COLLECTION); + PGPSecretKeyRing signingKey = secretKeys.getSecretKeyRing(signFp.getKeyId()); + PGPSecretKeyRing decryptionKey = secretKeys.getSecretKeyRing(cryptFp.getKeyId()); + SecretKeyRingProtector protector = SecretKeyRingProtector.unprotectedKeys(); + + // Encrypt and sign message + ByteArrayInputStream plaintextIn = new ByteArrayInputStream( + "This message was encrypted with a cert from a store and gets verified with a cert from a store as well".getBytes()); + ByteArrayOutputStream ciphertext = new ByteArrayOutputStream(); + EncryptionStream encryptionStream = PGPainless.encryptAndOrSign() + .onOutputStream(ciphertext) + .withOptions( + ProducerOptions.signAndEncrypt( + EncryptionOptions.encryptCommunications() + .addRecipient(adapter, cryptFp), + SigningOptions.get() + .addSignature(protector, signingKey) + )); + Streams.pipeAll(plaintextIn, encryptionStream); + encryptionStream.close(); + + // Prepare ciphertext for decryption + ByteArrayInputStream ciphertextIn = new ByteArrayInputStream(ciphertext.toByteArray()); + ByteArrayOutputStream plaintextOut = new ByteArrayOutputStream(); + // Decrypt and verify + DecryptionStream decryptionStream = PGPainless.decryptAndOrVerify() + .onInputStream(ciphertextIn) + .withOptions( + new ConsumerOptions() + .addDecryptionKey(decryptionKey, protector) + .addVerificationCerts(adapter)); + Streams.pipeAll(decryptionStream, plaintextOut); + decryptionStream.close(); + + // Check that message can be decrypted and is verified + OpenPgpMetadata result = decryptionStream.getResult(); + assertTrue(result.isEncrypted()); + assertTrue(result.isVerified()); + assertTrue(result.containsVerifiedSignatureFrom(signFp)); + } } diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/DetachedVerifyImpl.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/DetachedVerifyImpl.java index b563e704..3a5b3b6a 100644 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/DetachedVerifyImpl.java +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/DetachedVerifyImpl.java @@ -76,7 +76,7 @@ public class DetachedVerifyImpl implements DetachedVerify { verificationList.add(map(signatureVerification)); } - if (!options.getCertificates().isEmpty()) { + if (!options.getCertificateSource().getExplicitCertificates().isEmpty()) { if (verificationList.isEmpty()) { throw new SOPGPException.NoSignature(); } diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/InlineVerifyImpl.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/InlineVerifyImpl.java index 4948712c..82ed4282 100644 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/InlineVerifyImpl.java +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/InlineVerifyImpl.java @@ -74,7 +74,7 @@ public class InlineVerifyImpl implements InlineVerify { verificationList.add(map(signatureVerification)); } - if (!options.getCertificates().isEmpty()) { + if (!options.getCertificateSource().getExplicitCertificates().isEmpty()) { if (verificationList.isEmpty()) { throw new SOPGPException.NoSignature(); } From c19b8297a3fd09df528d03942f1396aa14de9738 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 29 Aug 2022 10:36:39 +0200 Subject: [PATCH 108/763] Add TODO for when bumping cert-d-java --- .../org/pgpainless/encryption_signing/EncryptionOptions.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/EncryptionOptions.java b/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/EncryptionOptions.java index 13131f08..8d7098d3 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/EncryptionOptions.java +++ b/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/EncryptionOptions.java @@ -258,6 +258,8 @@ public class EncryptionOptions { throws BadDataException, BadNameException, IOException { String fingerprint = certificateFingerprint.toString().toLowerCase(); Certificate certificateRecord = certificateStore.getCertificate(fingerprint); + // TODO: getCertificate throws NSEE automatically in 0.2.2+ + // Remove if statement below when bumping if (certificateRecord == null) { throw new NoSuchElementException("Cannot find certificate '" + certificateFingerprint + "'"); } From 43e0f43bd9ff96744adcaa1239a7281b45cc3237 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 1 Sep 2022 11:44:27 +0200 Subject: [PATCH 109/763] Bump cert-d-java to 0.2.1 and cert-d-pgpainless to 0.2.0 --- version.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/version.gradle b/version.gradle index 109e592e..eb20dc66 100644 --- a/version.gradle +++ b/version.gradle @@ -17,8 +17,8 @@ allprojects { junitVersion = '5.8.2' logbackVersion = '1.2.11' mockitoVersion = '4.5.1' - pgpainlessCertDVersion = '0.1.3-SNAPSHOT' - pgpCertDJavaVersion = '0.2.0' + pgpainlessCertDVersion = '0.2.0' + pgpCertDJavaVersion = '0.2.1' slf4jVersion = '1.7.36' sopJavaVersion = '4.0.7' } From a7929528455c5587b4e3064167970ba6aabeac10 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 1 Sep 2022 13:29:53 +0200 Subject: [PATCH 110/763] Remove code to manually throw NSEE for missing certs This is now done further down in the store itself --- .../org/pgpainless/encryption_signing/EncryptionOptions.java | 5 ----- 1 file changed, 5 deletions(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/EncryptionOptions.java b/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/EncryptionOptions.java index 8d7098d3..cf3b426a 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/EncryptionOptions.java +++ b/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/EncryptionOptions.java @@ -258,11 +258,6 @@ public class EncryptionOptions { throws BadDataException, BadNameException, IOException { String fingerprint = certificateFingerprint.toString().toLowerCase(); Certificate certificateRecord = certificateStore.getCertificate(fingerprint); - // TODO: getCertificate throws NSEE automatically in 0.2.2+ - // Remove if statement below when bumping - if (certificateRecord == null) { - throw new NoSuchElementException("Cannot find certificate '" + certificateFingerprint + "'"); - } PGPPublicKeyRing recipientCertificate = PGPainless.readKeyRing() .publicKeyRing(certificateRecord.getInputStream()); return addRecipient(recipientCertificate); From d7e4fcaec6293881e2de3cb75fde9e8c7b5b38d5 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Fri, 11 Nov 2022 14:20:17 +0100 Subject: [PATCH 111/763] OpenPgpMessageInputStream: Source verification certs from ConsumerOptions.getCertificateSource() --- .../OpenPgpMessageInputStream.java | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java index 28440257..65c8dad5 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java @@ -1018,11 +1018,9 @@ public class OpenPgpMessageInputStream extends DecryptionStream { } private PGPPublicKeyRing findCertificate(long keyId) { - for (PGPPublicKeyRing cert : options.getCertificates()) { - PGPPublicKey verificationKey = cert.getPublicKey(keyId); - if (verificationKey != null) { - return cert; - } + PGPPublicKeyRing cert = options.getCertificateSource().getCertificate(keyId); + if (cert != null) { + return cert; } if (options.getMissingCertificateCallback() != null) { From 3877410a65b0bb7c838417cb97403a4a056f535a Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Fri, 11 Nov 2022 14:28:33 +0100 Subject: [PATCH 112/763] Update CHANGELOG --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index cff696f1..6f19bc95 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,9 @@ SPDX-License-Identifier: CC0-1.0 to do decryption using session keys. This enables decryption of messages without encrypted session key packets. - Use BCs `PGPEncryptedDataList.isIntegrityProtected()` to check for integrity protection +- Depend on `pgp-certificate-store` +- Add `ConsumerOptions.addVerificationCerts(PGPCertificateStore)` to allow sourcing certificates from + e.g. a [certificate store implementation](https://github.com/pgpainless/cert-d-java). ## 1.4.0-rc1 - Reimplement message consumption via new `OpenPgpMessageInputStream` From 90c3a015770cc9f2b1aa4bafd4fe8be873c5f79b Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 14 Nov 2022 12:58:31 +0100 Subject: [PATCH 113/763] Add test to verify proper functionality of MatchMakingSecretKeyRingProtector --- ...oundTripInlineSignInlineVerifyCmdTest.java | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/RoundTripInlineSignInlineVerifyCmdTest.java b/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/RoundTripInlineSignInlineVerifyCmdTest.java index 3d7980b0..5d635beb 100644 --- a/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/RoundTripInlineSignInlineVerifyCmdTest.java +++ b/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/RoundTripInlineSignInlineVerifyCmdTest.java @@ -233,4 +233,32 @@ public class RoundTripInlineSignInlineVerifyCmdTest extends CLITest { String verificationString = verificationsOut.toString(); assertTrue(verificationString.contains(CERT_1_SIGNING_KEY)); } + + @Test + public void testUnlockKeyWithOneOfMultiplePasswords() throws IOException { + File key = writeFile("key.asc", KEY_1); + File wrong1 = writeFile("wrong_1", "BuzzAldr1n"); + File correct = writeFile("correct", KEY_1_PASSWORD); + File wrong2 = writeFile("wrong_2", "NeilArmstr0ng"); + + pipeStringToStdin(MESSAGE); + ByteArrayOutputStream ciphertextOut = pipeStdoutToStream(); + assertSuccess(executeCommand("inline-sign", + key.getAbsolutePath(), + "--with-key-password", wrong1.getAbsolutePath(), + "--with-key-password", correct.getAbsolutePath(), + "--with-key-password", wrong2.getAbsolutePath())); + + File cert = writeFile("cert.asc", CERT_1); + pipeStringToStdin(ciphertextOut.toString()); + ByteArrayOutputStream msgOut = pipeStdoutToStream(); + File verificationsFile = nonExistentFile("verifications"); + assertSuccess(executeCommand("inline-verify", + "--verifications-out", verificationsFile.getAbsolutePath(), + cert.getAbsolutePath())); + + assertEquals(MESSAGE, msgOut.toString()); + String verificationString = readStringFromFile(verificationsFile); + assertTrue(verificationString.contains(CERT_1_SIGNING_KEY)); + } } From 093d786329bdd1ae38177dbf49b26677622fecc1 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 15 Nov 2022 21:27:22 +0100 Subject: [PATCH 114/763] Doc: Add section about indirect data types --- docs/source/pgpainless-cli/usage.md | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/docs/source/pgpainless-cli/usage.md b/docs/source/pgpainless-cli/usage.md index 967bfc11..2045de6b 100644 --- a/docs/source/pgpainless-cli/usage.md +++ b/docs/source/pgpainless-cli/usage.md @@ -84,8 +84,24 @@ Exit Codes: 61 Input file does not exist 67 Cannot unlock password protected secret key 69 Unsupported subcommand - 71 Unsupported special prefix (e.g. "@env/@fd") of indirect parameter + 71 Unsupported special prefix (e.g. "@ENV/@FD") of indirect parameter 73 Ambiguous input (a filename matching the designator already exists) 79 Key is not signing capable Powered by picocli -``` \ No newline at end of file +``` + +## Indirect Data Types + +Some commands take options whose arguments are indirect data types. Those are arguments which are not used directly, +but instead they point to a place where the argument value can be sourced from, such as a file, an environment variable +or a file descriptor. + +It is important to keep in mind, that options like `--with-password` or `--with-key-password` are examples for such +indirect data types. If you want to unlock a key whose password is `sw0rdf1sh`, you *cannot* provide the password +like `--with-key-password sw0rdf1sh`, but instead you have to either write out the password into a file and provide +the file's path (e.g. `--with-key-password /path/to/file`), store the password in an environment variable and pass that +(e.g. `--with-key-password @ENV:myvar`), or provide a numbered file descriptor from which the password can be read +(e.g. `--with-key-password @FD:4`). + +Note, that environment variables and file descriptors can only be used to pass input data to the program. +For output parameters (e.g. `--verifications-out`) only file paths are allowed. From b7478729b13b8792b34aa64cd144c64ceeab78d1 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 15 Nov 2022 21:47:46 +0100 Subject: [PATCH 115/763] Update man pages Since commit 0e777de14f927ad4b265adfc5ba201be29b1bde1 in pgpainless/sop-java, man page generation is reproducible. This very commit adopts reproducible man pages for the first time --- pgpainless-cli/packaging/man/pgpainless-cli-armor.1 | 4 +--- pgpainless-cli/packaging/man/pgpainless-cli-dearmor.1 | 4 +--- pgpainless-cli/packaging/man/pgpainless-cli-decrypt.1 | 10 ++-------- pgpainless-cli/packaging/man/pgpainless-cli-encrypt.1 | 10 ++-------- .../packaging/man/pgpainless-cli-extract-cert.1 | 4 +--- .../packaging/man/pgpainless-cli-generate-completion.1 | 7 +++---- .../packaging/man/pgpainless-cli-generate-key.1 | 10 ++-------- pgpainless-cli/packaging/man/pgpainless-cli-help.1 | 7 +++---- .../packaging/man/pgpainless-cli-inline-detach.1 | 4 +--- .../packaging/man/pgpainless-cli-inline-sign.1 | 10 ++-------- .../packaging/man/pgpainless-cli-inline-verify.1 | 10 ++-------- pgpainless-cli/packaging/man/pgpainless-cli-sign.1 | 10 ++-------- pgpainless-cli/packaging/man/pgpainless-cli-verify.1 | 4 +--- pgpainless-cli/packaging/man/pgpainless-cli-version.1 | 4 +--- pgpainless-cli/packaging/man/pgpainless-cli.1 | 7 +++---- 15 files changed, 27 insertions(+), 78 deletions(-) diff --git a/pgpainless-cli/packaging/man/pgpainless-cli-armor.1 b/pgpainless-cli/packaging/man/pgpainless-cli-armor.1 index d85b6bf9..c34cf923 100644 --- a/pgpainless-cli/packaging/man/pgpainless-cli-armor.1 +++ b/pgpainless-cli/packaging/man/pgpainless-cli-armor.1 @@ -2,12 +2,11 @@ .\" Title: pgpainless-cli-armor .\" Author: [see the "AUTHOR(S)" section] .\" Generator: Asciidoctor 2.0.10 -.\" Date: 2022-11-06 .\" Manual: PGPainless-CLI Manual .\" Source: .\" Language: English .\" -.TH "PGPAINLESS\-CLI\-ARMOR" "1" "2022-11-06" "" "PGPainless\-CLI Manual" +.TH "PGPAINLESS\-CLI\-ARMOR" "1" "" "" "PGPainless\-CLI Manual" .ie \n(.g .ds Aq \(aq .el .ds Aq ' .ss \n[.ss] 0 @@ -43,5 +42,4 @@ Label to be used in the header and tail of the armoring .sp \fB\-\-stacktrace\fP .RS 4 -Print Stacktrace .RE \ No newline at end of file diff --git a/pgpainless-cli/packaging/man/pgpainless-cli-dearmor.1 b/pgpainless-cli/packaging/man/pgpainless-cli-dearmor.1 index 98fae5ea..19ef25f1 100644 --- a/pgpainless-cli/packaging/man/pgpainless-cli-dearmor.1 +++ b/pgpainless-cli/packaging/man/pgpainless-cli-dearmor.1 @@ -2,12 +2,11 @@ .\" Title: pgpainless-cli-dearmor .\" Author: [see the "AUTHOR(S)" section] .\" Generator: Asciidoctor 2.0.10 -.\" Date: 2022-11-06 .\" Manual: PGPainless-CLI Manual .\" Source: .\" Language: English .\" -.TH "PGPAINLESS\-CLI\-DEARMOR" "1" "2022-11-06" "" "PGPainless\-CLI Manual" +.TH "PGPAINLESS\-CLI\-DEARMOR" "1" "" "" "PGPainless\-CLI Manual" .ie \n(.g .ds Aq \(aq .el .ds Aq ' .ss \n[.ss] 0 @@ -38,5 +37,4 @@ pgpainless\-cli\-dearmor \- Remove ASCII Armor from standard input .sp \fB\-\-stacktrace\fP .RS 4 -Print Stacktrace .RE \ No newline at end of file diff --git a/pgpainless-cli/packaging/man/pgpainless-cli-decrypt.1 b/pgpainless-cli/packaging/man/pgpainless-cli-decrypt.1 index 400adcff..17d59134 100644 --- a/pgpainless-cli/packaging/man/pgpainless-cli-decrypt.1 +++ b/pgpainless-cli/packaging/man/pgpainless-cli-decrypt.1 @@ -2,12 +2,11 @@ .\" Title: pgpainless-cli-decrypt .\" Author: [see the "AUTHOR(S)" section] .\" Generator: Asciidoctor 2.0.10 -.\" Date: 2022-11-06 .\" Manual: PGPainless-CLI Manual .\" Source: .\" Language: English .\" -.TH "PGPAINLESS\-CLI\-DECRYPT" "1" "2022-11-06" "" "PGPainless\-CLI Manual" +.TH "PGPAINLESS\-CLI\-DECRYPT" "1" "" "" "PGPainless\-CLI Manual" .ie \n(.g .ds Aq \(aq .el .ds Aq ' .ss \n[.ss] 0 @@ -65,12 +64,7 @@ Defaults to beginning of time (\(aq\-\(aq). Can be used to learn the session key on successful decryption .RE .sp -\fB\-\-stacktrace\fP -.RS 4 -Print Stacktrace -.RE -.sp -\fB\-\-verify\-out\fP=\fIVERIFICATIONS\fP +\fB\-\-stacktrace\fP, \fB\-\-verify\-out, \-\-verifications\-out\fP=\fIVERIFICATIONS\fP .RS 4 Emits signature verification status to the designated output .RE diff --git a/pgpainless-cli/packaging/man/pgpainless-cli-encrypt.1 b/pgpainless-cli/packaging/man/pgpainless-cli-encrypt.1 index 5cb9495b..f1d804d0 100644 --- a/pgpainless-cli/packaging/man/pgpainless-cli-encrypt.1 +++ b/pgpainless-cli/packaging/man/pgpainless-cli-encrypt.1 @@ -2,12 +2,11 @@ .\" Title: pgpainless-cli-encrypt .\" Author: [see the "AUTHOR(S)" section] .\" Generator: Asciidoctor 2.0.10 -.\" Date: 2022-11-06 .\" Manual: PGPainless-CLI Manual .\" Source: .\" Language: English .\" -.TH "PGPAINLESS\-CLI\-ENCRYPT" "1" "2022-11-06" "" "PGPainless\-CLI Manual" +.TH "PGPAINLESS\-CLI\-ENCRYPT" "1" "" "" "PGPainless\-CLI Manual" .ie \n(.g .ds Aq \(aq .el .ds Aq ' .ss \n[.ss] 0 @@ -53,12 +52,7 @@ ASCII armor the output Sign the output with a private key .RE .sp -\fB\-\-stacktrace\fP -.RS 4 -Print Stacktrace -.RE -.sp -\fB\-\-with\-key\-password\fP=\fIPASSWORD\fP +\fB\-\-stacktrace\fP, \fB\-\-with\-key\-password\fP=\fIPASSWORD\fP .RS 4 Passphrase to unlock the secret key(s). .sp diff --git a/pgpainless-cli/packaging/man/pgpainless-cli-extract-cert.1 b/pgpainless-cli/packaging/man/pgpainless-cli-extract-cert.1 index 0482c183..dcaf6e71 100644 --- a/pgpainless-cli/packaging/man/pgpainless-cli-extract-cert.1 +++ b/pgpainless-cli/packaging/man/pgpainless-cli-extract-cert.1 @@ -2,12 +2,11 @@ .\" Title: pgpainless-cli-extract-cert .\" Author: [see the "AUTHOR(S)" section] .\" Generator: Asciidoctor 2.0.10 -.\" Date: 2022-11-06 .\" Manual: PGPainless-CLI Manual .\" Source: .\" Language: English .\" -.TH "PGPAINLESS\-CLI\-EXTRACT\-CERT" "1" "2022-11-06" "" "PGPainless\-CLI Manual" +.TH "PGPAINLESS\-CLI\-EXTRACT\-CERT" "1" "" "" "PGPainless\-CLI Manual" .ie \n(.g .ds Aq \(aq .el .ds Aq ' .ss \n[.ss] 0 @@ -43,5 +42,4 @@ ASCII armor the output .sp \fB\-\-stacktrace\fP .RS 4 -Print Stacktrace .RE \ No newline at end of file diff --git a/pgpainless-cli/packaging/man/pgpainless-cli-generate-completion.1 b/pgpainless-cli/packaging/man/pgpainless-cli-generate-completion.1 index 637b1231..5ab3d673 100644 --- a/pgpainless-cli/packaging/man/pgpainless-cli-generate-completion.1 +++ b/pgpainless-cli/packaging/man/pgpainless-cli-generate-completion.1 @@ -2,12 +2,11 @@ .\" Title: pgpainless-cli-generate-completion .\" Author: [see the "AUTHOR(S)" section] .\" Generator: Asciidoctor 2.0.10 -.\" Date: 2022-11-06 .\" Manual: PGPainless-CLI Manual .\" Source: generate-completion 4.6.3 .\" Language: English .\" -.TH "PGPAINLESS\-CLI\-GENERATE\-COMPLETION" "1" "2022-11-06" "generate\-completion 4.6.3" "PGPainless\-CLI Manual" +.TH "PGPAINLESS\-CLI\-GENERATE\-COMPLETION" "1" "" "generate\-completion 4.6.3" "PGPainless\-CLI Manual" .ie \n(.g .ds Aq \(aq .el .ds Aq ' .ss \n[.ss] 0 @@ -51,7 +50,7 @@ Show this help message and exit. .sp \fB\-\-stacktrace\fP .RS 4 -Print Stacktrace +Print stacktrace .RE .sp \fB\-V\fP, \fB\-\-version\fP @@ -142,7 +141,7 @@ Unsupported subcommand .sp \fB71\fP .RS 4 -Unsupported special prefix (e.g. "@env/@fd") of indirect parameter +Unsupported special prefix (e.g. "@ENV/@FD") of indirect parameter .RE .sp \fB73\fP diff --git a/pgpainless-cli/packaging/man/pgpainless-cli-generate-key.1 b/pgpainless-cli/packaging/man/pgpainless-cli-generate-key.1 index ef950df9..96b069f3 100644 --- a/pgpainless-cli/packaging/man/pgpainless-cli-generate-key.1 +++ b/pgpainless-cli/packaging/man/pgpainless-cli-generate-key.1 @@ -2,12 +2,11 @@ .\" Title: pgpainless-cli-generate-key .\" Author: [see the "AUTHOR(S)" section] .\" Generator: Asciidoctor 2.0.10 -.\" Date: 2022-11-06 .\" Manual: PGPainless-CLI Manual .\" Source: .\" Language: English .\" -.TH "PGPAINLESS\-CLI\-GENERATE\-KEY" "1" "2022-11-06" "" "PGPainless\-CLI Manual" +.TH "PGPAINLESS\-CLI\-GENERATE\-KEY" "1" "" "" "PGPainless\-CLI Manual" .ie \n(.g .ds Aq \(aq .el .ds Aq ' .ss \n[.ss] 0 @@ -42,12 +41,7 @@ pgpainless\-cli\-generate\-key \- Generate a secret key ASCII armor the output .RE .sp -\fB\-\-stacktrace\fP -.RS 4 -Print Stacktrace -.RE -.sp -\fB\-\-with\-key\-password\fP=\fIPASSWORD\fP +\fB\-\-stacktrace\fP, \fB\-\-with\-key\-password\fP=\fIPASSWORD\fP .RS 4 Password to protect the private key with .sp diff --git a/pgpainless-cli/packaging/man/pgpainless-cli-help.1 b/pgpainless-cli/packaging/man/pgpainless-cli-help.1 index 67ef1ce6..6152fc87 100644 --- a/pgpainless-cli/packaging/man/pgpainless-cli-help.1 +++ b/pgpainless-cli/packaging/man/pgpainless-cli-help.1 @@ -2,12 +2,11 @@ .\" Title: pgpainless-cli-help .\" Author: [see the "AUTHOR(S)" section] .\" Generator: Asciidoctor 2.0.10 -.\" Date: 2022-11-06 .\" Manual: PGPainless-CLI Manual .\" Source: .\" Language: English .\" -.TH "PGPAINLESS\-CLI\-HELP" "1" "2022-11-06" "" "PGPainless\-CLI Manual" +.TH "PGPAINLESS\-CLI\-HELP" "1" "" "" "PGPainless\-CLI Manual" .ie \n(.g .ds Aq \(aq .el .ds Aq ' .ss \n[.ss] 0 @@ -45,7 +44,7 @@ Show usage help for the help command and exit. .sp \fB\-\-stacktrace\fP .RS 4 -Print Stacktrace +Print stacktrace .RE .SH "ARGUMENTS" .sp @@ -137,7 +136,7 @@ Unsupported subcommand .sp \fB71\fP .RS 4 -Unsupported special prefix (e.g. "@env/@fd") of indirect parameter +Unsupported special prefix (e.g. "@ENV/@FD") of indirect parameter .RE .sp \fB73\fP diff --git a/pgpainless-cli/packaging/man/pgpainless-cli-inline-detach.1 b/pgpainless-cli/packaging/man/pgpainless-cli-inline-detach.1 index 3d356293..c5d9d983 100644 --- a/pgpainless-cli/packaging/man/pgpainless-cli-inline-detach.1 +++ b/pgpainless-cli/packaging/man/pgpainless-cli-inline-detach.1 @@ -2,12 +2,11 @@ .\" Title: pgpainless-cli-inline-detach .\" Author: [see the "AUTHOR(S)" section] .\" Generator: Asciidoctor 2.0.10 -.\" Date: 2022-11-06 .\" Manual: PGPainless-CLI Manual .\" Source: .\" Language: English .\" -.TH "PGPAINLESS\-CLI\-INLINE\-DETACH" "1" "2022-11-06" "" "PGPainless\-CLI Manual" +.TH "PGPAINLESS\-CLI\-INLINE\-DETACH" "1" "" "" "PGPainless\-CLI Manual" .ie \n(.g .ds Aq \(aq .el .ds Aq ' .ss \n[.ss] 0 @@ -48,5 +47,4 @@ Destination to which a detached signatures block will be written .sp \fB\-\-stacktrace\fP .RS 4 -Print Stacktrace .RE \ No newline at end of file diff --git a/pgpainless-cli/packaging/man/pgpainless-cli-inline-sign.1 b/pgpainless-cli/packaging/man/pgpainless-cli-inline-sign.1 index 3f653b40..7deb568c 100644 --- a/pgpainless-cli/packaging/man/pgpainless-cli-inline-sign.1 +++ b/pgpainless-cli/packaging/man/pgpainless-cli-inline-sign.1 @@ -2,12 +2,11 @@ .\" Title: pgpainless-cli-inline-sign .\" Author: [see the "AUTHOR(S)" section] .\" Generator: Asciidoctor 2.0.10 -.\" Date: 2022-11-06 .\" Manual: PGPainless-CLI Manual .\" Source: .\" Language: English .\" -.TH "PGPAINLESS\-CLI\-INLINE\-SIGN" "1" "2022-11-06" "" "PGPainless\-CLI Manual" +.TH "PGPAINLESS\-CLI\-INLINE\-SIGN" "1" "" "" "PGPainless\-CLI Manual" .ie \n(.g .ds Aq \(aq .el .ds Aq ' .ss \n[.ss] 0 @@ -56,12 +55,7 @@ If \(aq\-\-as=text\(aq and the input data is not valid UTF\-8, inline\-sign fail ASCII armor the output .RE .sp -\fB\-\-stacktrace\fP -.RS 4 -Print Stacktrace -.RE -.sp -\fB\-\-with\-key\-password\fP=\fIPASSWORD\fP +\fB\-\-stacktrace\fP, \fB\-\-with\-key\-password\fP=\fIPASSWORD\fP .RS 4 Passphrase to unlock the secret key(s). .sp diff --git a/pgpainless-cli/packaging/man/pgpainless-cli-inline-verify.1 b/pgpainless-cli/packaging/man/pgpainless-cli-inline-verify.1 index 73f8316d..d97f274d 100644 --- a/pgpainless-cli/packaging/man/pgpainless-cli-inline-verify.1 +++ b/pgpainless-cli/packaging/man/pgpainless-cli-inline-verify.1 @@ -2,12 +2,11 @@ .\" Title: pgpainless-cli-inline-verify .\" Author: [see the "AUTHOR(S)" section] .\" Generator: Asciidoctor 2.0.10 -.\" Date: 2022-11-06 .\" Manual: PGPainless-CLI Manual .\" Source: .\" Language: English .\" -.TH "PGPAINLESS\-CLI\-INLINE\-VERIFY" "1" "2022-11-06" "" "PGPainless\-CLI Manual" +.TH "PGPAINLESS\-CLI\-INLINE\-VERIFY" "1" "" "" "PGPainless\-CLI Manual" .ie \n(.g .ds Aq \(aq .el .ds Aq ' .ss \n[.ss] 0 @@ -57,12 +56,7 @@ Reject signatures with a creation date not in range. Defaults to beginning of time ("\-"). .RE .sp -\fB\-\-stacktrace\fP -.RS 4 -Print Stacktrace -.RE -.sp -\fB\-\-verifications\-out\fP=\fI\fP +\fB\-\-stacktrace\fP, \fB\-\-verifications\-out\fP=\fI\fP .RS 4 File to write details over successful verifications to .RE diff --git a/pgpainless-cli/packaging/man/pgpainless-cli-sign.1 b/pgpainless-cli/packaging/man/pgpainless-cli-sign.1 index 3c63dccf..6519e0ec 100644 --- a/pgpainless-cli/packaging/man/pgpainless-cli-sign.1 +++ b/pgpainless-cli/packaging/man/pgpainless-cli-sign.1 @@ -2,12 +2,11 @@ .\" Title: pgpainless-cli-sign .\" Author: [see the "AUTHOR(S)" section] .\" Generator: Asciidoctor 2.0.10 -.\" Date: 2022-11-06 .\" Manual: PGPainless-CLI Manual .\" Source: .\" Language: English .\" -.TH "PGPAINLESS\-CLI\-SIGN" "1" "2022-11-06" "" "PGPainless\-CLI Manual" +.TH "PGPAINLESS\-CLI\-SIGN" "1" "" "" "PGPainless\-CLI Manual" .ie \n(.g .ds Aq \(aq .el .ds Aq ' .ss \n[.ss] 0 @@ -56,12 +55,7 @@ Emits the digest algorithm used to the specified file in a way that can be used ASCII armor the output .RE .sp -\fB\-\-stacktrace\fP -.RS 4 -Print Stacktrace -.RE -.sp -\fB\-\-with\-key\-password\fP=\fIPASSWORD\fP +\fB\-\-stacktrace\fP, \fB\-\-with\-key\-password\fP=\fIPASSWORD\fP .RS 4 Passphrase to unlock the secret key(s). .sp diff --git a/pgpainless-cli/packaging/man/pgpainless-cli-verify.1 b/pgpainless-cli/packaging/man/pgpainless-cli-verify.1 index a07ef2d7..5cf0020c 100644 --- a/pgpainless-cli/packaging/man/pgpainless-cli-verify.1 +++ b/pgpainless-cli/packaging/man/pgpainless-cli-verify.1 @@ -2,12 +2,11 @@ .\" Title: pgpainless-cli-verify .\" Author: [see the "AUTHOR(S)" section] .\" Generator: Asciidoctor 2.0.10 -.\" Date: 2022-11-06 .\" Manual: PGPainless-CLI Manual .\" Source: .\" Language: English .\" -.TH "PGPAINLESS\-CLI\-VERIFY" "1" "2022-11-06" "" "PGPainless\-CLI Manual" +.TH "PGPAINLESS\-CLI\-VERIFY" "1" "" "" "PGPainless\-CLI Manual" .ie \n(.g .ds Aq \(aq .el .ds Aq ' .ss \n[.ss] 0 @@ -48,7 +47,6 @@ Defaults to beginning of time ("\-"). .sp \fB\-\-stacktrace\fP .RS 4 -Print Stacktrace .RE .SH "ARGUMENTS" .sp diff --git a/pgpainless-cli/packaging/man/pgpainless-cli-version.1 b/pgpainless-cli/packaging/man/pgpainless-cli-version.1 index 9e80f4b3..003e549f 100644 --- a/pgpainless-cli/packaging/man/pgpainless-cli-version.1 +++ b/pgpainless-cli/packaging/man/pgpainless-cli-version.1 @@ -2,12 +2,11 @@ .\" Title: pgpainless-cli-version .\" Author: [see the "AUTHOR(S)" section] .\" Generator: Asciidoctor 2.0.10 -.\" Date: 2022-11-06 .\" Manual: PGPainless-CLI Manual .\" Source: .\" Language: English .\" -.TH "PGPAINLESS\-CLI\-VERSION" "1" "2022-11-06" "" "PGPainless\-CLI Manual" +.TH "PGPAINLESS\-CLI\-VERSION" "1" "" "" "PGPainless\-CLI Manual" .ie \n(.g .ds Aq \(aq .el .ds Aq ' .ss \n[.ss] 0 @@ -48,5 +47,4 @@ Print an extended version string .sp \fB\-\-stacktrace\fP .RS 4 -Print Stacktrace .RE \ No newline at end of file diff --git a/pgpainless-cli/packaging/man/pgpainless-cli.1 b/pgpainless-cli/packaging/man/pgpainless-cli.1 index 14bd9fb9..686f728f 100644 --- a/pgpainless-cli/packaging/man/pgpainless-cli.1 +++ b/pgpainless-cli/packaging/man/pgpainless-cli.1 @@ -2,12 +2,11 @@ .\" Title: pgpainless-cli .\" Author: [see the "AUTHOR(S)" section] .\" Generator: Asciidoctor 2.0.10 -.\" Date: 2022-11-06 .\" Manual: PGPainless-CLI Manual .\" Source: .\" Language: English .\" -.TH "PGPAINLESS\-CLI" "1" "2022-11-06" "" "PGPainless\-CLI Manual" +.TH "PGPAINLESS\-CLI" "1" "" "" "PGPainless\-CLI Manual" .ie \n(.g .ds Aq \(aq .el .ds Aq ' .ss \n[.ss] 0 @@ -38,7 +37,7 @@ pgpainless\-cli \- Stateless OpenPGP Protocol .sp \fB\-\-stacktrace\fP .RS 4 -Print Stacktrace +Print stacktrace .RE .SH "COMMANDS" .sp @@ -195,7 +194,7 @@ Unsupported subcommand .sp \fB71\fP .RS 4 -Unsupported special prefix (e.g. "@env/@fd") of indirect parameter +Unsupported special prefix (e.g. "@ENV/@FD") of indirect parameter .RE .sp \fB73\fP From bfeed54ede095bfa70b394340edf21b277bce7ee Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 15 Nov 2022 21:52:02 +0100 Subject: [PATCH 116/763] Docs: Update output of pgpainless-cli help command --- docs/source/pgpainless-cli/usage.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/docs/source/pgpainless-cli/usage.md b/docs/source/pgpainless-cli/usage.md index 2045de6b..9d3f6d93 100644 --- a/docs/source/pgpainless-cli/usage.md +++ b/docs/source/pgpainless-cli/usage.md @@ -49,7 +49,10 @@ Hereafter, the program will be referred to as `pgpainless-cli`. ``` $ pgpainless-cli help Stateless OpenPGP Protocol -Usage: pgpainless-cli [COMMAND] +Usage: pgpainless-cli [--stacktrace] [COMMAND] + +Options: + --stacktrace Print Stacktrace Commands: help Display usage information for the specified subcommand @@ -68,7 +71,7 @@ Commands: version Display version information about the tool Exit Codes: - 0 Successful program execution. + 0 Successful program execution 1 Generic program error 3 Verification requested but no verifiable signature found 13 Unsupported asymmetric algorithm @@ -87,7 +90,6 @@ Exit Codes: 71 Unsupported special prefix (e.g. "@ENV/@FD") of indirect parameter 73 Ambiguous input (a filename matching the designator already exists) 79 Key is not signing capable -Powered by picocli ``` ## Indirect Data Types From 38a2162d6a2db64fe21f81ea9155f4e55e1ead96 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 15 Nov 2022 22:07:20 +0100 Subject: [PATCH 117/763] Docs: Document generation/updating of man pages --- docs/source/pgpainless-cli/usage.md | 30 +++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/docs/source/pgpainless-cli/usage.md b/docs/source/pgpainless-cli/usage.md index 9d3f6d93..dd56c8b1 100644 --- a/docs/source/pgpainless-cli/usage.md +++ b/docs/source/pgpainless-cli/usage.md @@ -42,6 +42,36 @@ $ gradle installDist Afterwards, an uncompressed distributable is installed in `build/install/`. To execute the application, you can call `build/install/bin/pgpainless-cli{.bat}` +Building / updating man pages is a two-step process. +The contents of the man pages is largely defined by the `sop-java-picocli` source code. + +In order to generate a fresh set of man pages from the `sop-java-picocli` source, you need to clone that repository +next to the `pgpainless` repository: +```shell +$ ls +pgpainless +$ git clone https://github.com/pgpainless/sop-java.git +$ ls +pgpainless sop-java +``` + +Next, you need to execute the `asciiDoctor` gradle task inside the sop-java repository: +```shell +$ cd sop-java +$ gradle asciiDoctor +``` + +This will generate generic sop manpages in `sop-java-picocli/build/docs/manpage/`. + +Next, you need to execute a script for converting the `sop` manpages to fit the `pgpainless-cli` command with the help +of a script in the `pgpainless` repository: +```shell +$ cd ../pgpainless/pgpainless-cli +$ ./rewriteManPages.sh +``` + +The resulting updated man pages are placed in `packaging/man/`. + ## Usage Hereafter, the program will be referred to as `pgpainless-cli`. From 936a3f654fa13886e3f625cd538d2e5dd2fd9d80 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 15 Nov 2022 22:28:18 +0100 Subject: [PATCH 118/763] Docs: Add usage examples for pgpainless-cli --- docs/source/pgpainless-cli/usage.md | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/docs/source/pgpainless-cli/usage.md b/docs/source/pgpainless-cli/usage.md index dd56c8b1..4bc1d166 100644 --- a/docs/source/pgpainless-cli/usage.md +++ b/docs/source/pgpainless-cli/usage.md @@ -122,6 +122,24 @@ Exit Codes: 79 Key is not signing capable ``` +To get help on a subcommand, e.g. `encrypt`, just call the help subcommand followed by the subcommand you +are interested in (e.g. `pgpainless-cli help encrypt`). + +## Examples +```shell +$ # Generate a key +$ pgpainless-cli generate-key "Alice " > key.asc +$ # Extract a certificate from a key +$ cat key.asc | pgpainless-cli extract-cert > cert.asc +$ # Create an encrypted signed message +$ echo "Hello, World!" | pgpainless-cli encrypt cert.asc --sign-with key.asc > msg.asc +$ # Decrypt an encrypted message and verify the signature +$ cat msg.asc | pgpainless-cli decrypt key.asc --verify-with cert.asc --verifications-out verifications.txt +Hello, World! +$ cat verifications.txt +2022-11-15T21:25:48Z 4FF67C69150209ED8139DE22578CB2FABD5D7897 9000235358B8CEA6A368EC86DE56DC2D942ACAA4 +``` + ## Indirect Data Types Some commands take options whose arguments are indirect data types. Those are arguments which are not used directly, From 847d4b5e3327772bd67de33e9776f0c128591453 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 15 Nov 2022 16:05:29 +0100 Subject: [PATCH 119/763] Update SECURITY.md --- SECURITY.md | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/SECURITY.md b/SECURITY.md index 59e9adbf..fad439b5 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -12,11 +12,12 @@ SPDX-License-Identifier: Apache-2.0 Use this section to tell people about which versions of your project are currently being supported with security updates. -| Version | Supported | -|---------| ------------------ | -| 1.1.X | :white_check_mark: | -| 1.0.X | :white_check_mark: | -| < 1.0.0 | :x: | +| Version | Supported | +|----------|--------------------| +| 1.4.X-rc | :white_check_mark: | +| 1.3.X | :white_check_mark: | +| 1.2.X | :white_check_mark: | +| < 1.2.0 | :x: | ## Reporting a Vulnerability From 03d04fb324672c1532488a7faba62c1a17007dc6 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 15 Nov 2022 16:29:24 +0100 Subject: [PATCH 120/763] Tests: Replace usages of default algorithm policies with specific policies --- .../encryption_signing/EncryptDecryptTest.java | 4 ++-- .../org/pgpainless/example/ManagePolicy.java | 17 +++++++++-------- .../generation/GeneratingWeakKeyThrowsTest.java | 10 ++++++---- .../modification/RefuseToAddWeakSubkeyTest.java | 4 ++-- .../java/org/pgpainless/policy/PolicyTest.java | 2 +- 5 files changed, 20 insertions(+), 17 deletions(-) diff --git a/pgpainless-core/src/test/java/org/pgpainless/encryption_signing/EncryptDecryptTest.java b/pgpainless-core/src/test/java/org/pgpainless/encryption_signing/EncryptDecryptTest.java index 88a2c38b..216d0c65 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/encryption_signing/EncryptDecryptTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/encryption_signing/EncryptDecryptTest.java @@ -66,9 +66,9 @@ public class EncryptDecryptTest { @BeforeEach public void setDefaultPolicy() { PGPainless.getPolicy().setSymmetricKeyEncryptionAlgorithmPolicy( - Policy.SymmetricKeyAlgorithmPolicy.defaultSymmetricKeyEncryptionAlgorithmPolicy()); + Policy.SymmetricKeyAlgorithmPolicy.symmetricKeyEncryptionPolicy2022()); PGPainless.getPolicy().setSymmetricKeyDecryptionAlgorithmPolicy( - Policy.SymmetricKeyAlgorithmPolicy.defaultSymmetricKeyDecryptionAlgorithmPolicy()); + Policy.SymmetricKeyAlgorithmPolicy.symmetricKeyDecryptionPolicy2022()); } @TestTemplate diff --git a/pgpainless-core/src/test/java/org/pgpainless/example/ManagePolicy.java b/pgpainless-core/src/test/java/org/pgpainless/example/ManagePolicy.java index fa2759dc..711de225 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/example/ManagePolicy.java +++ b/pgpainless-core/src/test/java/org/pgpainless/example/ManagePolicy.java @@ -44,22 +44,22 @@ public class ManagePolicy { public void resetPolicy() { // Policy for hash algorithms in non-revocation signatures PGPainless.getPolicy().setSignatureHashAlgorithmPolicy( - Policy.HashAlgorithmPolicy.defaultSignatureAlgorithmPolicy()); + Policy.HashAlgorithmPolicy.static2022SignatureHashAlgorithmPolicy()); // Policy for hash algorithms in revocation signatures PGPainless.getPolicy().setRevocationSignatureHashAlgorithmPolicy( - Policy.HashAlgorithmPolicy.defaultRevocationSignatureHashAlgorithmPolicy()); + Policy.HashAlgorithmPolicy.static2022RevocationSignatureHashAlgorithmPolicy()); // Policy for public key algorithms and bit lengths PGPainless.getPolicy().setPublicKeyAlgorithmPolicy( - Policy.PublicKeyAlgorithmPolicy.defaultPublicKeyAlgorithmPolicy()); + Policy.PublicKeyAlgorithmPolicy.bsi2021PublicKeyAlgorithmPolicy()); // Policy for acceptable symmetric encryption algorithms when decrypting messages PGPainless.getPolicy().setSymmetricKeyDecryptionAlgorithmPolicy( - Policy.SymmetricKeyAlgorithmPolicy.defaultSymmetricKeyDecryptionAlgorithmPolicy()); + Policy.SymmetricKeyAlgorithmPolicy.symmetricKeyDecryptionPolicy2022()); // Policy for acceptable symmetric encryption algorithms when encrypting messages PGPainless.getPolicy().setSymmetricKeyEncryptionAlgorithmPolicy( - Policy.SymmetricKeyAlgorithmPolicy.defaultSymmetricKeyEncryptionAlgorithmPolicy()); + Policy.SymmetricKeyAlgorithmPolicy.symmetricKeyEncryptionPolicy2022()); // Policy for acceptable compression algorithms PGPainless.getPolicy().setCompressionAlgorithmPolicy( - Policy.CompressionAlgorithmPolicy.defaultCompressionAlgorithmPolicy()); + Policy.CompressionAlgorithmPolicy.anyCompressionAlgorithmPolicy()); // Known notations PGPainless.getPolicy().getNotationRegistry().clear(); } @@ -73,7 +73,7 @@ public class ManagePolicy { * * Per default, PGPainless will reject non-revocation signatures that use SHA-1 as hash algorithm. * To inspect PGPainless' default signature hash algorithm policy, see - * {@link Policy.HashAlgorithmPolicy#defaultSignatureAlgorithmPolicy()}. + * {@link Policy.HashAlgorithmPolicy#static2022SignatureHashAlgorithmPolicy()}. * * Since it may be a valid use-case to accept signatures made using SHA-1 as part of a less strict policy, * this example demonstrates how to set a custom signature hash algorithm policy. @@ -108,7 +108,8 @@ public class ManagePolicy { /** * Similar to hash algorithms, {@link PublicKeyAlgorithm PublicKeyAlgorithms} tend to get outdated eventually. * Per default, PGPainless will reject signatures made by keys of unacceptable algorithm or length. - * See {@link Policy.PublicKeyAlgorithmPolicy#defaultPublicKeyAlgorithmPolicy()} to inspect PGPainless' defaults. + * See {@link Policy.PublicKeyAlgorithmPolicy#bsi2021PublicKeyAlgorithmPolicy()} + * to inspect PGPainless' defaults. * * This example demonstrates how to set a custom public key algorithm policy. */ diff --git a/pgpainless-core/src/test/java/org/pgpainless/key/generation/GeneratingWeakKeyThrowsTest.java b/pgpainless-core/src/test/java/org/pgpainless/key/generation/GeneratingWeakKeyThrowsTest.java index aa79add5..6fbf0572 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/key/generation/GeneratingWeakKeyThrowsTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/key/generation/GeneratingWeakKeyThrowsTest.java @@ -26,7 +26,7 @@ public class GeneratingWeakKeyThrowsTest { public void refuseToGenerateWeakPrimaryKeyTest() { // ensure we have default public key algorithm policy set PGPainless.getPolicy().setPublicKeyAlgorithmPolicy( - Policy.PublicKeyAlgorithmPolicy.defaultPublicKeyAlgorithmPolicy()); + Policy.PublicKeyAlgorithmPolicy.bsi2021PublicKeyAlgorithmPolicy()); assertThrows(IllegalArgumentException.class, () -> PGPainless.buildKeyRing() @@ -38,7 +38,7 @@ public class GeneratingWeakKeyThrowsTest { public void refuseToAddWeakSubkeyDuringGenerationTest() { // ensure we have default public key algorithm policy set PGPainless.getPolicy().setPublicKeyAlgorithmPolicy( - Policy.PublicKeyAlgorithmPolicy.defaultPublicKeyAlgorithmPolicy()); + Policy.PublicKeyAlgorithmPolicy.bsi2021PublicKeyAlgorithmPolicy()); KeyRingBuilder kb = PGPainless.buildKeyRing() .setPrimaryKey(KeySpec.getBuilder(KeyType.RSA(RsaLength._4096), @@ -50,7 +50,8 @@ public class GeneratingWeakKeyThrowsTest { } @Test - public void allowToAddWeakKeysWithWeakPolicy() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException { + public void allowToAddWeakKeysWithWeakPolicy() + throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException { // set a weak algorithm policy Map bitStrengths = new HashMap<>(); bitStrengths.put(PublicKeyAlgorithm.RSA_GENERAL, 512); @@ -67,6 +68,7 @@ public class GeneratingWeakKeyThrowsTest { .build(); // reset public key algorithm policy - PGPainless.getPolicy().setPublicKeyAlgorithmPolicy(Policy.PublicKeyAlgorithmPolicy.defaultPublicKeyAlgorithmPolicy()); + PGPainless.getPolicy().setPublicKeyAlgorithmPolicy( + Policy.PublicKeyAlgorithmPolicy.bsi2021PublicKeyAlgorithmPolicy()); } } diff --git a/pgpainless-core/src/test/java/org/pgpainless/key/modification/RefuseToAddWeakSubkeyTest.java b/pgpainless-core/src/test/java/org/pgpainless/key/modification/RefuseToAddWeakSubkeyTest.java index 2570837f..73c5953c 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/key/modification/RefuseToAddWeakSubkeyTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/key/modification/RefuseToAddWeakSubkeyTest.java @@ -34,7 +34,7 @@ public class RefuseToAddWeakSubkeyTest { public void testEditorRefusesToAddWeakSubkey() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException { // ensure default policy is set - PGPainless.getPolicy().setPublicKeyAlgorithmPolicy(Policy.PublicKeyAlgorithmPolicy.defaultPublicKeyAlgorithmPolicy()); + PGPainless.getPolicy().setPublicKeyAlgorithmPolicy(Policy.PublicKeyAlgorithmPolicy.bsi2021PublicKeyAlgorithmPolicy()); PGPSecretKeyRing secretKeys = PGPainless.generateKeyRing() .modernKeyRing("Alice"); @@ -84,6 +84,6 @@ public class RefuseToAddWeakSubkeyTest { assertEquals(2, PGPainless.inspectKeyRing(secretKeys).getEncryptionSubkeys(EncryptionPurpose.ANY).size()); // reset default policy - PGPainless.getPolicy().setPublicKeyAlgorithmPolicy(Policy.PublicKeyAlgorithmPolicy.defaultPublicKeyAlgorithmPolicy()); + PGPainless.getPolicy().setPublicKeyAlgorithmPolicy(Policy.PublicKeyAlgorithmPolicy.bsi2021PublicKeyAlgorithmPolicy()); } } diff --git a/pgpainless-core/src/test/java/org/pgpainless/policy/PolicyTest.java b/pgpainless-core/src/test/java/org/pgpainless/policy/PolicyTest.java index 4e3cb51e..aa7078e4 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/policy/PolicyTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/policy/PolicyTest.java @@ -56,7 +56,7 @@ public class PolicyTest { policy.setRevocationSignatureHashAlgorithmPolicy(new Policy.HashAlgorithmPolicy(HashAlgorithm.SHA512, revHashAlgoMap)); - policy.setPublicKeyAlgorithmPolicy(Policy.PublicKeyAlgorithmPolicy.defaultPublicKeyAlgorithmPolicy()); + policy.setPublicKeyAlgorithmPolicy(Policy.PublicKeyAlgorithmPolicy.bsi2021PublicKeyAlgorithmPolicy()); } @Test From e976cc6dd244ce61b165984a46f24f1f061e55bc Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 15 Nov 2022 16:46:17 +0100 Subject: [PATCH 121/763] Move getResult() method around --- .../OpenPgpMessageInputStream.java | 54 +++++++++---------- 1 file changed, 27 insertions(+), 27 deletions(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java index 65c8dad5..a73b5154 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java @@ -799,6 +799,33 @@ public class OpenPgpMessageInputStream extends DecryptionStream { return new MessageMetadata((MessageMetadata.Message) metadata); } + @Override + public OpenPgpMetadata getResult() { + MessageMetadata m = getMetadata(); + resultBuilder.setCompressionAlgorithm(m.getCompressionAlgorithm()); + resultBuilder.setModificationDate(m.getModificationDate()); + resultBuilder.setFileName(m.getFilename()); + resultBuilder.setFileEncoding(m.getFormat()); + resultBuilder.setSessionKey(m.getSessionKey()); + resultBuilder.setDecryptionKey(m.getDecryptionKey()); + + for (SignatureVerification accepted : m.getVerifiedDetachedSignatures()) { + resultBuilder.addVerifiedDetachedSignature(accepted); + } + for (SignatureVerification.Failure rejected : m.getRejectedDetachedSignatures()) { + resultBuilder.addInvalidDetachedSignature(rejected.getSignatureVerification(), rejected.getValidationException()); + } + + for (SignatureVerification accepted : m.getVerifiedInlineSignatures()) { + resultBuilder.addVerifiedInbandSignature(accepted); + } + for (SignatureVerification.Failure rejected : m.getRejectedInlineSignatures()) { + resultBuilder.addInvalidInbandSignature(rejected.getSignatureVerification(), rejected.getValidationException()); + } + + return resultBuilder.build(); + } + private static class SortedESKs { private final List skesks = new ArrayList<>(); @@ -832,33 +859,6 @@ public class OpenPgpMessageInputStream extends DecryptionStream { } } - @Override - public OpenPgpMetadata getResult() { - MessageMetadata m = getMetadata(); - resultBuilder.setCompressionAlgorithm(m.getCompressionAlgorithm()); - resultBuilder.setModificationDate(m.getModificationDate()); - resultBuilder.setFileName(m.getFilename()); - resultBuilder.setFileEncoding(m.getFormat()); - resultBuilder.setSessionKey(m.getSessionKey()); - resultBuilder.setDecryptionKey(m.getDecryptionKey()); - - for (SignatureVerification accepted : m.getVerifiedDetachedSignatures()) { - resultBuilder.addVerifiedDetachedSignature(accepted); - } - for (SignatureVerification.Failure rejected : m.getRejectedDetachedSignatures()) { - resultBuilder.addInvalidDetachedSignature(rejected.getSignatureVerification(), rejected.getValidationException()); - } - - for (SignatureVerification accepted : m.getVerifiedInlineSignatures()) { - resultBuilder.addVerifiedInbandSignature(accepted); - } - for (SignatureVerification.Failure rejected : m.getRejectedInlineSignatures()) { - resultBuilder.addInvalidInbandSignature(rejected.getSignatureVerification(), rejected.getValidationException()); - } - - return resultBuilder.build(); - } - // In 'OPS LIT("Foo") SIG', OPS is only updated with "Foo" // In 'OPS[1] OPS LIT("Foo") SIG SIG', OPS[1] (nested) is updated with OPS LIT("Foo") SIG. // Therefore, we need to handle the innermost signature layer differently when updating with Literal data. From 3023d532e3a36e793538380915b7e0b81716fa5d Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 16 Nov 2022 15:32:15 +0100 Subject: [PATCH 122/763] Make DecryptionStream.getMetadata() first-class, deprecate getResult() --- .../CloseForResultInputStream.java | 39 ------------------- .../DecryptionStream.java | 12 ++++-- .../MessageMetadata.java | 35 +++++++++++++++++ .../OpenPgpMessageInputStream.java | 33 ++-------------- 4 files changed, 46 insertions(+), 73 deletions(-) delete mode 100644 pgpainless-core/src/main/java/org/pgpainless/decryption_verification/CloseForResultInputStream.java diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/CloseForResultInputStream.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/CloseForResultInputStream.java deleted file mode 100644 index ba895a08..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/CloseForResultInputStream.java +++ /dev/null @@ -1,39 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.decryption_verification; - -import java.io.IOException; -import java.io.InputStream; -import javax.annotation.Nonnull; - -public abstract class CloseForResultInputStream extends InputStream { - - protected final OpenPgpMetadata.Builder resultBuilder; - private boolean isClosed = false; - - public CloseForResultInputStream(@Nonnull OpenPgpMetadata.Builder resultBuilder) { - this.resultBuilder = resultBuilder; - } - - @Override - public void close() throws IOException { - this.isClosed = true; - } - - /** - * Return the result of the decryption. - * The result contains metadata about the decryption, such as signatures, used keys and algorithms, as well as information - * about the decrypted file/stream. - * - * Can only be obtained once the stream got successfully closed ({@link #close()}). - * @return metadata - */ - public OpenPgpMetadata getResult() { - if (!isClosed) { - throw new IllegalStateException("Stream MUST be closed before the result can be accessed."); - } - return resultBuilder.build(); - } -} diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionStream.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionStream.java index 0f221ad7..e3a51bb9 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionStream.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionStream.java @@ -4,10 +4,14 @@ package org.pgpainless.decryption_verification; -import javax.annotation.Nonnull; +import java.io.InputStream; -public abstract class DecryptionStream extends CloseForResultInputStream { - public DecryptionStream(@Nonnull OpenPgpMetadata.Builder resultBuilder) { - super(resultBuilder); +public abstract class DecryptionStream extends InputStream { + + public abstract MessageMetadata getMetadata(); + + @Deprecated + public OpenPgpMetadata getResult() { + return getMetadata().toLegacyMetadata(); } } diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/MessageMetadata.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/MessageMetadata.java index 7811578e..26439b40 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/MessageMetadata.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/MessageMetadata.java @@ -300,9 +300,16 @@ public class MessageMetadata { public static class Message extends Layer { + protected boolean cleartextSigned; + public Message() { super(0); } + + public boolean isCleartextSigned() { + return cleartextSigned; + } + } public static class LiteralData implements Nested { @@ -453,4 +460,32 @@ public class MessageMetadata { abstract O getProperty(Layer last); } + public OpenPgpMetadata toLegacyMetadata() { + OpenPgpMetadata.Builder resultBuilder = OpenPgpMetadata.getBuilder(); + resultBuilder.setCompressionAlgorithm(getCompressionAlgorithm()); + resultBuilder.setModificationDate(getModificationDate()); + resultBuilder.setFileName(getFilename()); + resultBuilder.setFileEncoding(getFormat()); + resultBuilder.setSessionKey(getSessionKey()); + resultBuilder.setDecryptionKey(getDecryptionKey()); + + for (SignatureVerification accepted : getVerifiedDetachedSignatures()) { + resultBuilder.addVerifiedDetachedSignature(accepted); + } + for (SignatureVerification.Failure rejected : getRejectedDetachedSignatures()) { + resultBuilder.addInvalidDetachedSignature(rejected.getSignatureVerification(), rejected.getValidationException()); + } + + for (SignatureVerification accepted : getVerifiedInlineSignatures()) { + resultBuilder.addVerifiedInbandSignature(accepted); + } + for (SignatureVerification.Failure rejected : getRejectedInlineSignatures()) { + resultBuilder.addInvalidInbandSignature(rejected.getSignatureVerification(), rejected.getValidationException()); + } + if (message.isCleartextSigned()) { + resultBuilder.setCleartextSigned(); + } + + return resultBuilder.build(); + } } diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java index a73b5154..a820dca5 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java @@ -156,6 +156,7 @@ public class OpenPgpMessageInputStream extends DecryptionStream { if (openPgpIn.isAsciiArmored()) { ArmoredInputStream armorIn = ArmoredInputStreamFactory.get(openPgpIn); if (armorIn.isClearText()) { + ((MessageMetadata.Message) metadata).cleartextSigned = true; return new OpenPgpMessageInputStream(Type.cleartext_signed, armorIn, options, metadata, policy); } else { @@ -173,7 +174,7 @@ public class OpenPgpMessageInputStream extends DecryptionStream { @Nonnull MessageMetadata.Layer metadata, @Nonnull Policy policy) throws PGPException, IOException { - super(OpenPgpMetadata.getBuilder()); + super(); this.policy = policy; this.options = options; @@ -203,7 +204,7 @@ public class OpenPgpMessageInputStream extends DecryptionStream { @Nonnull ConsumerOptions options, @Nonnull MessageMetadata.Layer metadata, @Nonnull Policy policy) throws PGPException, IOException { - super(OpenPgpMetadata.getBuilder()); + super(); this.policy = policy; this.options = options; this.metadata = metadata; @@ -226,7 +227,6 @@ public class OpenPgpMessageInputStream extends DecryptionStream { // Cleartext Signature Framework (probably signed message) case cleartext_signed: - resultBuilder.setCleartextSigned(); MultiPassStrategy multiPassStrategy = options.getMultiPassStrategy(); PGPSignatureList detachedSignatures = ClearsignedMessageUtil .detachSignaturesFromInbandClearsignedMessage( @@ -799,33 +799,6 @@ public class OpenPgpMessageInputStream extends DecryptionStream { return new MessageMetadata((MessageMetadata.Message) metadata); } - @Override - public OpenPgpMetadata getResult() { - MessageMetadata m = getMetadata(); - resultBuilder.setCompressionAlgorithm(m.getCompressionAlgorithm()); - resultBuilder.setModificationDate(m.getModificationDate()); - resultBuilder.setFileName(m.getFilename()); - resultBuilder.setFileEncoding(m.getFormat()); - resultBuilder.setSessionKey(m.getSessionKey()); - resultBuilder.setDecryptionKey(m.getDecryptionKey()); - - for (SignatureVerification accepted : m.getVerifiedDetachedSignatures()) { - resultBuilder.addVerifiedDetachedSignature(accepted); - } - for (SignatureVerification.Failure rejected : m.getRejectedDetachedSignatures()) { - resultBuilder.addInvalidDetachedSignature(rejected.getSignatureVerification(), rejected.getValidationException()); - } - - for (SignatureVerification accepted : m.getVerifiedInlineSignatures()) { - resultBuilder.addVerifiedInbandSignature(accepted); - } - for (SignatureVerification.Failure rejected : m.getRejectedInlineSignatures()) { - resultBuilder.addInvalidInbandSignature(rejected.getSignatureVerification(), rejected.getValidationException()); - } - - return resultBuilder.build(); - } - private static class SortedESKs { private final List skesks = new ArrayList<>(); From 33d9a784bb34da7b68200cfc4d3714fcf13aed03 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 16 Nov 2022 17:20:43 +0100 Subject: [PATCH 123/763] Add javadoc to MEssageMetadata class --- .../MessageMetadata.java | 368 +++++++++++++++--- 1 file changed, 312 insertions(+), 56 deletions(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/MessageMetadata.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/MessageMetadata.java index 26439b40..1bf22b47 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/MessageMetadata.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/MessageMetadata.java @@ -19,6 +19,9 @@ import java.util.Iterator; import java.util.List; import java.util.NoSuchElementException; +/** + * View for extracting metadata about a {@link Message}. + */ public class MessageMetadata { protected Message message; @@ -27,6 +30,57 @@ public class MessageMetadata { this.message = message; } + /** + * Convert this {@link MessageMetadata} object into a legacy {@link OpenPgpMetadata} object. + * This method is intended to be used for a transition period between the 1.3 / 1.4+ branches. + * TODO: Remove in 1.5.X + * + * @return converted {@link OpenPgpMetadata} object + */ + public @Nonnull OpenPgpMetadata toLegacyMetadata() { + OpenPgpMetadata.Builder resultBuilder = OpenPgpMetadata.getBuilder(); + resultBuilder.setCompressionAlgorithm(getCompressionAlgorithm()); + Date modDate = getModificationDate(); + if (modDate != null) { + resultBuilder.setModificationDate(modDate); + } + String fileName = getFilename(); + if (fileName != null) { + resultBuilder.setFileName(fileName); + } + StreamEncoding encoding = getFormat(); + if (encoding != null) { + resultBuilder.setFileEncoding(encoding); + } + resultBuilder.setSessionKey(getSessionKey()); + resultBuilder.setDecryptionKey(getDecryptionKey()); + + for (SignatureVerification accepted : getVerifiedDetachedSignatures()) { + resultBuilder.addVerifiedDetachedSignature(accepted); + } + for (SignatureVerification.Failure rejected : getRejectedDetachedSignatures()) { + resultBuilder.addInvalidDetachedSignature(rejected.getSignatureVerification(), rejected.getValidationException()); + } + + for (SignatureVerification accepted : getVerifiedInlineSignatures()) { + resultBuilder.addVerifiedInbandSignature(accepted); + } + for (SignatureVerification.Failure rejected : getRejectedInlineSignatures()) { + resultBuilder.addInvalidInbandSignature(rejected.getSignatureVerification(), rejected.getValidationException()); + } + if (message.isCleartextSigned()) { + resultBuilder.setCleartextSigned(); + } + + return resultBuilder.build(); + } + + /** + * Return the {@link SymmetricKeyAlgorithm} of the outermost encrypted data packet, or null if message is + * unencrypted. + * + * @return encryption algorithm + */ public @Nullable SymmetricKeyAlgorithm getEncryptionAlgorithm() { Iterator algorithms = getEncryptionAlgorithms(); if (algorithms.hasNext()) { @@ -35,6 +89,14 @@ public class MessageMetadata { return null; } + /** + * Return an {@link Iterator} of {@link SymmetricKeyAlgorithm SymmetricKeyAlgorithms} encountered in the message. + * The first item returned by the iterator is the algorithm of the outermost encrypted data packet, the next item + * that of the next nested encrypted data packet and so on. + * The iterator might also be empty, in case of an unencrypted message. + * + * @return iterator of symmetric encryption algorithms + */ public @Nonnull Iterator getEncryptionAlgorithms() { return new LayerIterator(message) { @Override @@ -49,6 +111,12 @@ public class MessageMetadata { }; } + /** + * Return the {@link CompressionAlgorithm} of the outermost compressed data packet, or null, if the message + * does not contain any compressed data packets. + * + * @return compression algorithm + */ public @Nullable CompressionAlgorithm getCompressionAlgorithm() { Iterator algorithms = getCompressionAlgorithms(); if (algorithms.hasNext()) { @@ -57,6 +125,14 @@ public class MessageMetadata { return null; } + /** + * Return an {@link Iterator} of {@link CompressionAlgorithm CompressionAlgorithms} encountered in the message. + * The first item returned by the iterator is the algorithm of the outermost compressed data packet, the next + * item that of the next nested compressed data packet and so on. + * The iterator might also be empty, in case of a message without any compressed data packets. + * + * @return iterator of compression algorithms + */ public @Nonnull Iterator getCompressionAlgorithms() { return new LayerIterator(message) { @Override @@ -71,6 +147,12 @@ public class MessageMetadata { }; } + /** + * Return the {@link SessionKey} of the outermost encrypted data packet. + * If the message was unencrypted, this method returns
null
. + * + * @return session key of the message + */ public @Nullable SessionKey getSessionKey() { Iterator sessionKeys = getSessionKeys(); if (sessionKeys.hasNext()) { @@ -79,6 +161,14 @@ public class MessageMetadata { return null; } + /** + * Return an {@link Iterator} of {@link SessionKey SessionKeys} for all encrypted data packets in the message. + * The first item returned by the iterator is the session key of the outermost encrypted data packet, + * the next item that of the next nested encrypted data packet and so on. + * The iterator might also be empty, in case of an unencrypted message. + * + * @return iterator of session keys + */ public @Nonnull Iterator getSessionKeys() { return new LayerIterator(message) { @Override @@ -93,14 +183,31 @@ public class MessageMetadata { }; } - public List getVerifiedDetachedSignatures() { - return new ArrayList<>(message.verifiedDetachedSignatures); + /** + * Return a list of all verified detached signatures. + * This list contains all acceptable, correct detached signatures. + * + * @return verified detached signatures + */ + public @Nonnull List getVerifiedDetachedSignatures() { + return message.getVerifiedDetachedSignatures(); } - public List getRejectedDetachedSignatures() { - return new ArrayList<>(message.rejectedDetachedSignatures); + /** + * Return a list of all rejected detached signatures. + * + * @return rejected detached signatures + */ + public @Nonnull List getRejectedDetachedSignatures() { + return message.getRejectedDetachedSignatures(); } + /** + * Return a list of all verified inline-signatures. + * This list contains all acceptable, correct signatures that were part of the message itself. + * + * @return verified inline signatures + */ public @Nonnull List getVerifiedInlineSignatures() { List verifications = new ArrayList<>(); Iterator> verificationsByLayer = getVerifiedInlineSignaturesByLayer(); @@ -110,6 +217,16 @@ public class MessageMetadata { return verifications; } + /** + * Return an {@link Iterator} of {@link List Lists} of verified inline-signatures of the message. + * Since signatures might occur in different layers within a message, this method can be used to gain more detailed + * insights into what signatures were encountered at what layers of the message structure. + * Each item of the {@link Iterator} represents a layer of the message and contains only signatures from + * this layer. + * An empty list means no (or no acceptable) signatures were encountered in that layer. + * + * @return iterator of lists of signatures by-layer. + */ public @Nonnull Iterator> getVerifiedInlineSignaturesByLayer() { return new LayerIterator>(message) { @Override @@ -132,6 +249,11 @@ public class MessageMetadata { }; } + /** + * Return a list of all rejected inline-signatures of the message. + * + * @return list of rejected inline-signatures + */ public @Nonnull List getRejectedInlineSignatures() { List rejected = new ArrayList<>(); Iterator> rejectedByLayer = getRejectedInlineSignaturesByLayer(); @@ -141,6 +263,12 @@ public class MessageMetadata { return rejected; } + /** + * Similar to {@link #getVerifiedInlineSignaturesByLayer()}, this method returns all rejected inline-signatures + * of the message, but organized by layer. + * + * @return rejected inline-signatures by-layer + */ public @Nonnull Iterator> getRejectedInlineSignaturesByLayer() { return new LayerIterator>(message) { @Override @@ -163,7 +291,16 @@ public class MessageMetadata { }; } - public String getFilename() { + /** + * Return the value of the literal data packet's filename field. + * This value can be used to store a decrypted file under its original filename, + * but since this field is not necessarily part of the signed data of a message, usage of this field is + * discouraged. + * + * @return filename + * @see RFC4880 §5.9. Literal Data Packet + */ + public @Nullable String getFilename() { LiteralData literalData = findLiteralData(); if (literalData == null) { return null; @@ -171,7 +308,15 @@ public class MessageMetadata { return literalData.getFileName(); } - public Date getModificationDate() { + /** + * Return the value of the literal data packets modification date field. + * This value can be used to restore the modification date of a decrypted file, + * but since this field is not necessarily part of the signed data, its use is discouraged. + * + * @return modification date + * @see RFC4880 §5.9. Literal Data Packet + */ + public @Nullable Date getModificationDate() { LiteralData literalData = findLiteralData(); if (literalData == null) { return null; @@ -179,7 +324,15 @@ public class MessageMetadata { return literalData.getModificationDate(); } - public StreamEncoding getFormat() { + /** + * Return the value of the format field of the literal data packet. + * This value indicates what format (text, binary data, ...) the data has. + * Since this field is not necessarily part of the signed data of a message, its usage is discouraged. + * + * @return format + * @see RFC4880 §5.9. Literal Data Packet + */ + public @Nullable StreamEncoding getFormat() { LiteralData literalData = findLiteralData(); if (literalData == null) { return null; @@ -187,19 +340,33 @@ public class MessageMetadata { return literalData.getFormat(); } - private LiteralData findLiteralData() { - Nested nested = message.child; + /** + * Find the {@link LiteralData} layer of an OpenPGP message. + * Usually, every message has a literal data packet, but for malformed messages this method might still + * return
null
. + * + * @return literal data + */ + private @Nullable LiteralData findLiteralData() { + Nested nested = message.getChild(); if (nested == null) { return null; } - while (nested.hasNestedChild()) { + while (nested != null && nested.hasNestedChild()) { Layer layer = (Layer) nested; - nested = layer.child; + nested = layer.getChild(); } return (LiteralData) nested; } + /** + * Return the {@link SubkeyIdentifier} of the decryption key that was used to decrypt the outermost encryption + * layer. + * If the message was unencrypted, this might return
null
. + * + * @return decryption key + */ public SubkeyIdentifier getDecryptionKey() { Iterator iterator = new LayerIterator(message) { @Override @@ -236,58 +403,130 @@ public class MessageMetadata { } } - public Nested getChild() { + /** + * Return the nested child element of this layer. + * Might return
null
, if this layer does not have a child element + * (e.g. if this is a {@link LiteralData} packet). + * + * @return child element + */ + public @Nullable Nested getChild() { return child; } - public void setChild(Nested child) { + /** + * Set the nested child element for this layer. + * + * @param child child element + */ + void setChild(Nested child) { this.child = child; } + /** + * Return a list of all verified detached signatures of this layer. + * + * @return all verified detached signatures of this layer + */ public List getVerifiedDetachedSignatures() { return new ArrayList<>(verifiedDetachedSignatures); } + /** + * Return a list of all rejected detached signatures of this layer. + * + * @return all rejected detached signatures of this layer + */ public List getRejectedDetachedSignatures() { return new ArrayList<>(rejectedDetachedSignatures); } + /** + * Add a verified detached signature for this layer. + * + * @param signatureVerification verified detached signature + */ void addVerifiedDetachedSignature(SignatureVerification signatureVerification) { verifiedDetachedSignatures.add(signatureVerification); } + /** + * Add a rejected detached signature for this layer. + * + * @param failure rejected detached signature + */ void addRejectedDetachedSignature(SignatureVerification.Failure failure) { rejectedDetachedSignatures.add(failure); } + /** + * Return a list of all verified one-pass-signatures of this layer. + * + * @return all verified one-pass-signatures of this layer + */ public List getVerifiedOnePassSignatures() { return new ArrayList<>(verifiedOnePassSignatures); } + /** + * Return a list of all rejected one-pass-signatures of this layer. + * + * @return all rejected one-pass-signatures of this layer + */ public List getRejectedOnePassSignatures() { return new ArrayList<>(rejectedOnePassSignatures); } + /** + * Add a verified one-pass-signature for this layer. + * + * @param verifiedOnePassSignature verified one-pass-signature for this layer + */ void addVerifiedOnePassSignature(SignatureVerification verifiedOnePassSignature) { this.verifiedOnePassSignatures.add(verifiedOnePassSignature); } + /** + * Add a rejected one-pass-signature for this layer. + * + * @param rejected rejected one-pass-signature for this layer + */ void addRejectedOnePassSignature(SignatureVerification.Failure rejected) { this.rejectedOnePassSignatures.add(rejected); } + /** + * Return a list of all verified prepended signatures of this layer. + * + * @return all verified prepended signatures of this layer + */ public List getVerifiedPrependedSignatures() { return new ArrayList<>(verifiedPrependedSignatures); } + /** + * Return a list of all rejected prepended signatures of this layer. + * + * @return all rejected prepended signatures of this layer + */ public List getRejectedPrependedSignatures() { return new ArrayList<>(rejectedPrependedSignatures); } + /** + * Add a verified prepended signature for this layer. + * + * @param verified verified prepended signature + */ void addVerifiedPrependedSignature(SignatureVerification verified) { this.verifiedPrependedSignatures.add(verified); } + /** + * Add a rejected prepended signature for this layer. + * + * @param rejected rejected prepended signature + */ void addRejectedPrependedSignature(SignatureVerification.Failure rejected) { this.rejectedPrependedSignatures.add(rejected); } @@ -306,6 +545,12 @@ public class MessageMetadata { super(0); } + /** + * Returns true, is the message is a signed message using the cleartext signature framework. + * + * @return
true
if message is cleartext-signed,
false
otherwise + * @see RFC4880 §7. Cleartext Signature Framework + */ public boolean isCleartextSigned() { return cleartextSigned; } @@ -321,26 +566,46 @@ public class MessageMetadata { this("", new Date(0L), StreamEncoding.BINARY); } - public LiteralData(String fileName, Date modificationDate, StreamEncoding format) { + public LiteralData(@Nonnull String fileName, + @Nonnull Date modificationDate, + @Nonnull StreamEncoding format) { this.fileName = fileName; this.modificationDate = modificationDate; this.format = format; } - public String getFileName() { + /** + * Return the value of the filename field. + * An empty String
""
indicates no filename. + * + * @return filename + */ + public @Nonnull String getFileName() { return fileName; } - public Date getModificationDate() { + /** + * Return the value of the modification date field. + * A special date
{@code new Date(0L)}
indicates no modification date. + * + * @return modification date + */ + public @Nonnull Date getModificationDate() { return modificationDate; } - public StreamEncoding getFormat() { + /** + * Return the value of the format field. + * + * @return format + */ + public @Nonnull StreamEncoding getFormat() { return format; } @Override public boolean hasNestedChild() { + // A literal data packet MUST NOT have a child element, as its content is the plaintext return false; } } @@ -348,17 +613,22 @@ public class MessageMetadata { public static class CompressedData extends Layer implements Nested { protected final CompressionAlgorithm algorithm; - public CompressedData(CompressionAlgorithm zip, int depth) { + public CompressedData(@Nonnull CompressionAlgorithm zip, int depth) { super(depth); this.algorithm = zip; } - public CompressionAlgorithm getAlgorithm() { + /** + * Return the {@link CompressionAlgorithm} used to compress the packet. + * @return compression algorithm + */ + public @Nonnull CompressionAlgorithm getAlgorithm() { return algorithm; } @Override public boolean hasNestedChild() { + // A compressed data packet MUST have a child element return true; } } @@ -369,25 +639,40 @@ public class MessageMetadata { protected SessionKey sessionKey; protected List recipients; - public EncryptedData(SymmetricKeyAlgorithm algorithm, int depth) { + public EncryptedData(@Nonnull SymmetricKeyAlgorithm algorithm, int depth) { super(depth); this.algorithm = algorithm; } - public SymmetricKeyAlgorithm getAlgorithm() { + /** + * Return the {@link SymmetricKeyAlgorithm} used to encrypt the packet. + * @return symmetric encryption algorithm + */ + public @Nonnull SymmetricKeyAlgorithm getAlgorithm() { return algorithm; } - public SessionKey getSessionKey() { + /** + * Return the {@link SessionKey} used to decrypt the packet. + * + * @return session key + */ + public @Nonnull SessionKey getSessionKey() { return sessionKey; } - public List getRecipients() { + /** + * Return a list of all recipient key ids to which the packet was encrypted for. + * + * @return recipients + */ + public @Nonnull List getRecipients() { return new ArrayList<>(recipients); } @Override public boolean hasNestedChild() { + // An encrypted data packet MUST have a child element return true; } } @@ -398,10 +683,10 @@ public class MessageMetadata { Layer last = null; Message parent; - LayerIterator(Message message) { + LayerIterator(@Nonnull Message message) { super(); this.parent = message; - this.current = message.child; + this.current = message.getChild(); if (matches(current)) { last = (Layer) current; } @@ -437,8 +722,8 @@ public class MessageMetadata { } private void findNext() { - while (current instanceof Layer) { - current = ((Layer) current).child; + while (current != null && current instanceof Layer) { + current = ((Layer) current).getChild(); if (matches(current)) { last = (Layer) current; break; @@ -459,33 +744,4 @@ public class MessageMetadata { abstract O getProperty(Layer last); } - - public OpenPgpMetadata toLegacyMetadata() { - OpenPgpMetadata.Builder resultBuilder = OpenPgpMetadata.getBuilder(); - resultBuilder.setCompressionAlgorithm(getCompressionAlgorithm()); - resultBuilder.setModificationDate(getModificationDate()); - resultBuilder.setFileName(getFilename()); - resultBuilder.setFileEncoding(getFormat()); - resultBuilder.setSessionKey(getSessionKey()); - resultBuilder.setDecryptionKey(getDecryptionKey()); - - for (SignatureVerification accepted : getVerifiedDetachedSignatures()) { - resultBuilder.addVerifiedDetachedSignature(accepted); - } - for (SignatureVerification.Failure rejected : getRejectedDetachedSignatures()) { - resultBuilder.addInvalidDetachedSignature(rejected.getSignatureVerification(), rejected.getValidationException()); - } - - for (SignatureVerification accepted : getVerifiedInlineSignatures()) { - resultBuilder.addVerifiedInbandSignature(accepted); - } - for (SignatureVerification.Failure rejected : getRejectedInlineSignatures()) { - resultBuilder.addInvalidInbandSignature(rejected.getSignatureVerification(), rejected.getValidationException()); - } - if (message.isCleartextSigned()) { - resultBuilder.setCleartextSigned(); - } - - return resultBuilder.build(); - } } From 70cca563d7dd0ab87e61c0a7b0f41bf83e5bc220 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 16 Nov 2022 17:24:26 +0100 Subject: [PATCH 124/763] Add javadoc to getMetadata() and getResult() --- .../decryption_verification/DecryptionStream.java | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionStream.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionStream.java index e3a51bb9..c967ba10 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionStream.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionStream.java @@ -8,8 +8,21 @@ import java.io.InputStream; public abstract class DecryptionStream extends InputStream { + /** + * Return {@link MessageMetadata metadata} about the decrypted / verified message. + * The {@link DecryptionStream} MUST be closed via {@link #close()} before the metadata object can be accessed. + * + * @return message metadata + */ public abstract MessageMetadata getMetadata(); + /** + * Return a {@link OpenPgpMetadata} object containing information about the decrypted / verified message. + * The {@link DecryptionStream} MUST be closed via {@link #close()} before the metadata object can be accessed. + * + * @return message metadata + * @deprecated use {@link #getMetadata()} instead. + */ @Deprecated public OpenPgpMetadata getResult() { return getMetadata().toLegacyMetadata(); From 14376048360331d8204979a8e3dc83f74458a604 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 16 Nov 2022 17:36:25 +0100 Subject: [PATCH 125/763] Add documentation to DecryptionStream --- .../pgpainless/decryption_verification/DecryptionStream.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionStream.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionStream.java index c967ba10..28642bbf 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionStream.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionStream.java @@ -6,6 +6,9 @@ package org.pgpainless.decryption_verification; import java.io.InputStream; +/** + * Abstract definition of an {@link InputStream} which can be used to decrypt / verify OpenPGP messages. + */ public abstract class DecryptionStream extends InputStream { /** From fd2f6523ecb8caea3f830fbb63384e11e79808f8 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 16 Nov 2022 17:36:51 +0100 Subject: [PATCH 126/763] More specific exception message for when nesting depth is exceeded --- .../org/pgpainless/decryption_verification/MessageMetadata.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/MessageMetadata.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/MessageMetadata.java index 1bf22b47..3af2a4d3 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/MessageMetadata.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/MessageMetadata.java @@ -399,7 +399,7 @@ public class MessageMetadata { public Layer(int depth) { this.depth = depth; if (depth > MAX_LAYER_DEPTH) { - throw new MalformedOpenPgpMessageException("Maximum nesting depth exceeded."); + throw new MalformedOpenPgpMessageException("Maximum packet nesting depth (" + MAX_LAYER_DEPTH + ") exceeded."); } } From 8faec25ecf1b39c888f0da138c0366b4e4307f30 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 16 Nov 2022 17:38:38 +0100 Subject: [PATCH 127/763] Enable previously disabled test for marker+seipd packet processing --- .../java/org/pgpainless/signature/IgnoreMarkerPackets.java | 3 --- 1 file changed, 3 deletions(-) diff --git a/pgpainless-core/src/test/java/org/pgpainless/signature/IgnoreMarkerPackets.java b/pgpainless-core/src/test/java/org/pgpainless/signature/IgnoreMarkerPackets.java index 87766dfe..c86efd05 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/signature/IgnoreMarkerPackets.java +++ b/pgpainless-core/src/test/java/org/pgpainless/signature/IgnoreMarkerPackets.java @@ -20,7 +20,6 @@ import org.bouncycastle.openpgp.PGPPublicKeyRing; import org.bouncycastle.openpgp.PGPSecretKeyRing; import org.bouncycastle.openpgp.PGPSignature; import org.bouncycastle.util.io.Streams; -import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.pgpainless.PGPainless; import org.pgpainless.decryption_verification.ConsumerOptions; @@ -160,8 +159,6 @@ public class IgnoreMarkerPackets { } @Test - @Disabled - // TODO: Enable and fix public void markerPlusEncryptedMessage() throws IOException, PGPException { String msg = "-----BEGIN PGP MESSAGE-----\n" + "\n" + From b95568f30a82e38f028b7c3dcb8d1c4de96cf3f5 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 16 Nov 2022 17:39:48 +0100 Subject: [PATCH 128/763] Rename IgnoreMarkerPacketsTest --- .../{IgnoreMarkerPackets.java => IgnoreMarkerPacketsTest.java} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename pgpainless-core/src/test/java/org/pgpainless/signature/{IgnoreMarkerPackets.java => IgnoreMarkerPacketsTest.java} (99%) diff --git a/pgpainless-core/src/test/java/org/pgpainless/signature/IgnoreMarkerPackets.java b/pgpainless-core/src/test/java/org/pgpainless/signature/IgnoreMarkerPacketsTest.java similarity index 99% rename from pgpainless-core/src/test/java/org/pgpainless/signature/IgnoreMarkerPackets.java rename to pgpainless-core/src/test/java/org/pgpainless/signature/IgnoreMarkerPacketsTest.java index c86efd05..6ece093b 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/signature/IgnoreMarkerPackets.java +++ b/pgpainless-core/src/test/java/org/pgpainless/signature/IgnoreMarkerPacketsTest.java @@ -33,7 +33,7 @@ import org.pgpainless.key.util.KeyRingUtils; * * @see Sequoia Test-Suite */ -public class IgnoreMarkerPackets { +public class IgnoreMarkerPacketsTest { private static final String KEY = "-----BEGIN PGP PRIVATE KEY BLOCK-----\n" + "Comment: Bob's OpenPGP Transferable Secret Key\n" + From 4e4c095d8dfe4f55dd7c0ffbf85e5e3cb24dc49c Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 16 Nov 2022 17:40:51 +0100 Subject: [PATCH 129/763] Rename tests to end in Test --- ...pientMessage.java => DecryptHiddenRecipientMessageTest.java} | 2 +- ...va => RejectWeakSymmetricAlgorithmDuringDecryptionTest.java} | 2 +- ... SignedMessageVerificationWithoutCertIsStillSignedTest.java} | 2 +- ...allback.java => VerifyWithMissingPublicKeyCallbackTest.java} | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) rename pgpainless-core/src/test/java/org/pgpainless/decryption_verification/{DecryptHiddenRecipientMessage.java => DecryptHiddenRecipientMessageTest.java} (99%) rename pgpainless-core/src/test/java/org/pgpainless/decryption_verification/{RejectWeakSymmetricAlgorithmDuringDecryption.java => RejectWeakSymmetricAlgorithmDuringDecryptionTest.java} (99%) rename pgpainless-core/src/test/java/org/pgpainless/decryption_verification/{SignedMessageVerificationWithoutCertIsStillSigned.java => SignedMessageVerificationWithoutCertIsStillSignedTest.java} (96%) rename pgpainless-core/src/test/java/org/pgpainless/decryption_verification/{VerifyWithMissingPublicKeyCallback.java => VerifyWithMissingPublicKeyCallbackTest.java} (98%) diff --git a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/DecryptHiddenRecipientMessage.java b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/DecryptHiddenRecipientMessageTest.java similarity index 99% rename from pgpainless-core/src/test/java/org/pgpainless/decryption_verification/DecryptHiddenRecipientMessage.java rename to pgpainless-core/src/test/java/org/pgpainless/decryption_verification/DecryptHiddenRecipientMessageTest.java index 5c63a996..5400d17c 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/DecryptHiddenRecipientMessage.java +++ b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/DecryptHiddenRecipientMessageTest.java @@ -24,7 +24,7 @@ import org.pgpainless.key.SubkeyIdentifier; import org.pgpainless.key.info.KeyRingInfo; import org.pgpainless.util.TestAllImplementations; -public class DecryptHiddenRecipientMessage { +public class DecryptHiddenRecipientMessageTest { @TestTemplate @ExtendWith(TestAllImplementations.class) diff --git a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/RejectWeakSymmetricAlgorithmDuringDecryption.java b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/RejectWeakSymmetricAlgorithmDuringDecryptionTest.java similarity index 99% rename from pgpainless-core/src/test/java/org/pgpainless/decryption_verification/RejectWeakSymmetricAlgorithmDuringDecryption.java rename to pgpainless-core/src/test/java/org/pgpainless/decryption_verification/RejectWeakSymmetricAlgorithmDuringDecryptionTest.java index 53c690ed..1201ef69 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/RejectWeakSymmetricAlgorithmDuringDecryption.java +++ b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/RejectWeakSymmetricAlgorithmDuringDecryptionTest.java @@ -22,7 +22,7 @@ import org.pgpainless.exception.UnacceptableAlgorithmException; * Test PGPainless' default symmetric key algorithm policy for decryption of messages. * The default decryption policy rejects messages encrypted with IDEA and TripleDES, as well as unencrypted messages. */ -public class RejectWeakSymmetricAlgorithmDuringDecryption { +public class RejectWeakSymmetricAlgorithmDuringDecryptionTest { private static final String key = "-----BEGIN PGP PRIVATE KEY BLOCK-----\n" + " Comment: Bob's OpenPGP Transferable Secret Key\n" + diff --git a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/SignedMessageVerificationWithoutCertIsStillSigned.java b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/SignedMessageVerificationWithoutCertIsStillSignedTest.java similarity index 96% rename from pgpainless-core/src/test/java/org/pgpainless/decryption_verification/SignedMessageVerificationWithoutCertIsStillSigned.java rename to pgpainless-core/src/test/java/org/pgpainless/decryption_verification/SignedMessageVerificationWithoutCertIsStillSignedTest.java index 0b2f5d7d..27bc9954 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/SignedMessageVerificationWithoutCertIsStillSigned.java +++ b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/SignedMessageVerificationWithoutCertIsStillSignedTest.java @@ -17,7 +17,7 @@ import org.bouncycastle.util.io.Streams; import org.junit.jupiter.api.Test; import org.pgpainless.PGPainless; -public class SignedMessageVerificationWithoutCertIsStillSigned { +public class SignedMessageVerificationWithoutCertIsStillSignedTest { private static final String message = "-----BEGIN PGP MESSAGE-----\n" + "\n" + diff --git a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/VerifyWithMissingPublicKeyCallback.java b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/VerifyWithMissingPublicKeyCallbackTest.java similarity index 98% rename from pgpainless-core/src/test/java/org/pgpainless/decryption_verification/VerifyWithMissingPublicKeyCallback.java rename to pgpainless-core/src/test/java/org/pgpainless/decryption_verification/VerifyWithMissingPublicKeyCallbackTest.java index bcbad822..14136650 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/VerifyWithMissingPublicKeyCallback.java +++ b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/VerifyWithMissingPublicKeyCallbackTest.java @@ -38,7 +38,7 @@ import org.pgpainless.key.util.KeyRingUtils; * a signature is encountered which was made by a key that was not provided in * {@link ConsumerOptions#addVerificationCert(PGPPublicKeyRing)}. */ -public class VerifyWithMissingPublicKeyCallback { +public class VerifyWithMissingPublicKeyCallbackTest { @Test public void testMissingPublicKeyCallback() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, IOException { From 6ba7e91f2a57eecdd510ebcae3b178a1116ca432 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 16 Nov 2022 18:00:44 +0100 Subject: [PATCH 130/763] Add documentation and removal-TODO to old OpenPgpMetadata class --- .../pgpainless/decryption_verification/OpenPgpMetadata.java | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMetadata.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMetadata.java index 130ea554..e26fb804 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMetadata.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMetadata.java @@ -27,6 +27,12 @@ import org.pgpainless.key.OpenPgpFingerprint; import org.pgpainless.key.SubkeyIdentifier; import org.pgpainless.util.SessionKey; +/** + * Legacy class containing metadata about an OpenPGP message. + * It is advised to use {@link MessageMetadata} instead. + * + * TODO: Remove in 1.5.X + */ public class OpenPgpMetadata { private final Set recipientKeyIds; From b9152d5cdea2b31a4dcc66c2126d8540d3b52365 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 17 Nov 2022 14:28:29 +0100 Subject: [PATCH 131/763] SOP: Add test to ensure that armoring already-armored data is idempotent --- .../java/org/pgpainless/cli/commands/ArmorCmdTest.java | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/ArmorCmdTest.java b/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/ArmorCmdTest.java index c1fb810f..afd5ded4 100644 --- a/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/ArmorCmdTest.java +++ b/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/ArmorCmdTest.java @@ -97,4 +97,14 @@ public class ArmorCmdTest extends CLITest { assertEquals(SOPGPException.UnsupportedOption.EXIT_CODE, exitCode); assertEquals(0, out.size()); } + + @Test + public void armorAlreadyArmoredDataIsIdempotent() throws IOException { + pipeStringToStdin(key); + ByteArrayOutputStream armorOut = pipeStdoutToStream(); + assertSuccess(executeCommand("armor")); + + String armored = armorOut.toString(); + assertEquals(key, armored); + } } From de76f4b3a9f79a3a3bf7b52548925ac7a005348d Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Fri, 18 Nov 2022 14:17:47 +0100 Subject: [PATCH 132/763] Fix indentation for CLI tests --- .../java/org/pgpainless/cli/commands/CLITest.java | 3 ++- .../cli/commands/ExtractCertCmdTest.java | 3 ++- .../cli/commands/GenerateKeyCmdTest.java | 6 ++++-- .../commands/RoundTripEncryptDecryptCmdTest.java | 15 ++++++++++----- .../cli/commands/RoundTripSignVerifyCmdTest.java | 3 ++- 5 files changed, 20 insertions(+), 10 deletions(-) diff --git a/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/CLITest.java b/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/CLITest.java index d5d076cc..9e2fd895 100644 --- a/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/CLITest.java +++ b/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/CLITest.java @@ -161,6 +161,7 @@ public abstract class CLITest { } public void assertSuccess(int exitCode) { - assertEquals(0, exitCode, "Expected successful program execution"); + assertEquals(0, exitCode, + "Expected successful program execution"); } } diff --git a/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/ExtractCertCmdTest.java b/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/ExtractCertCmdTest.java index 3eebcb47..f1f69912 100644 --- a/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/ExtractCertCmdTest.java +++ b/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/ExtractCertCmdTest.java @@ -30,7 +30,8 @@ public class ExtractCertCmdTest extends CLITest { } @Test - public void testExtractCert() throws InvalidAlgorithmParameterException, NoSuchAlgorithmException, PGPException, IOException { + public void testExtractCert() + throws InvalidAlgorithmParameterException, NoSuchAlgorithmException, PGPException, IOException { PGPSecretKeyRing secretKeys = PGPainless.generateKeyRing() .simpleEcKeyRing("Juliet Capulet "); diff --git a/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/GenerateKeyCmdTest.java b/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/GenerateKeyCmdTest.java index 2e4e6e7f..302e4b8f 100644 --- a/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/GenerateKeyCmdTest.java +++ b/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/GenerateKeyCmdTest.java @@ -83,7 +83,8 @@ public class GenerateKeyCmdTest extends CLITest { KeyRingInfo info = PGPainless.inspectKeyRing(secretKeys); assertTrue(info.isFullyEncrypted()); - assertNotNull(UnlockSecretKey.unlockSecretKey(secretKeys.getSecretKey(), Passphrase.fromPassword("sw0rdf1sh"))); + assertNotNull(UnlockSecretKey + .unlockSecretKey(secretKeys.getSecretKey(), Passphrase.fromPassword("sw0rdf1sh"))); } @Test @@ -91,6 +92,7 @@ public class GenerateKeyCmdTest extends CLITest { int exit = executeCommand("generate-key", "--with-key-password", "nonexistent", "Alice "); - assertEquals(SOPGPException.MissingInput.EXIT_CODE, exit, "Expected MISSING_INPUT (" + SOPGPException.MissingInput.EXIT_CODE + ")"); + assertEquals(SOPGPException.MissingInput.EXIT_CODE, exit, + "Expected MISSING_INPUT (" + SOPGPException.MissingInput.EXIT_CODE + ")"); } } diff --git a/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/RoundTripEncryptDecryptCmdTest.java b/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/RoundTripEncryptDecryptCmdTest.java index 5d183ea6..9122829a 100644 --- a/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/RoundTripEncryptDecryptCmdTest.java +++ b/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/RoundTripEncryptDecryptCmdTest.java @@ -168,7 +168,8 @@ public class RoundTripEncryptDecryptCmdTest extends CLITest { File verifications = nonExistentFile("verifications"); ByteArrayOutputStream out = pipeStdoutToStream(); - int exitCode = executeCommand("decrypt", "--verifications-out", verifications.getAbsolutePath(), key.getAbsolutePath()); + int exitCode = executeCommand("decrypt", "--verifications-out", + verifications.getAbsolutePath(), key.getAbsolutePath()); assertEquals(SOPGPException.IncompleteVerification.EXIT_CODE, exitCode); assertEquals(0, out.size()); @@ -211,7 +212,8 @@ public class RoundTripEncryptDecryptCmdTest extends CLITest { pipeStringToStdin(ciphertext); ByteArrayOutputStream plaintextOut = pipeStdoutToStream(); - assertSuccess(executeCommand("decrypt", "--session-key-out", sessionKeyFile.getAbsolutePath(), key.getAbsolutePath())); + assertSuccess(executeCommand("decrypt", "--session-key-out", + sessionKeyFile.getAbsolutePath(), key.getAbsolutePath())); assertEquals(plaintext, plaintextOut.toString()); String resultSessionKey = readStringFromFile(sessionKeyFile); @@ -300,7 +302,8 @@ public class RoundTripEncryptDecryptCmdTest extends CLITest { InvalidAlgorithmParameterException, NoSuchAlgorithmException, IOException { PGPSecretKeyRing secretKeys = PGPainless.buildKeyRing() .addUserId("No Crypt ") - .setPrimaryKey(KeySpec.getBuilder(KeyType.EDDSA(EdDSACurve._Ed25519), KeyFlag.CERTIFY_OTHER, KeyFlag.SIGN_DATA)) + .setPrimaryKey(KeySpec.getBuilder(KeyType.EDDSA(EdDSACurve._Ed25519), + KeyFlag.CERTIFY_OTHER, KeyFlag.SIGN_DATA)) .build(); PGPPublicKeyRing cert = PGPainless.extractCertificate(secretKeys); File certFile = writeFile("cert.pgp", cert.getEncoded()); @@ -314,11 +317,13 @@ public class RoundTripEncryptDecryptCmdTest extends CLITest { } @Test - public void testSignWithIncapableKey() throws IOException, PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException { + public void testSignWithIncapableKey() + throws IOException, PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException { PGPSecretKeyRing secretKeys = PGPainless.buildKeyRing() .addUserId("Cannot Sign ") .setPrimaryKey(KeySpec.getBuilder(KeyType.EDDSA(EdDSACurve._Ed25519), KeyFlag.CERTIFY_OTHER)) - .addSubkey(KeySpec.getBuilder(KeyType.XDH(XDHSpec._X25519), KeyFlag.ENCRYPT_COMMS, KeyFlag.ENCRYPT_STORAGE)) + .addSubkey(KeySpec.getBuilder( + KeyType.XDH(XDHSpec._X25519), KeyFlag.ENCRYPT_COMMS, KeyFlag.ENCRYPT_STORAGE)) .build(); File keyFile = writeFile("key.pgp", secretKeys.getEncoded()); File certFile = writeFile("cert.pgp", PGPainless.extractCertificate(secretKeys).getEncoded()); diff --git a/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/RoundTripSignVerifyCmdTest.java b/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/RoundTripSignVerifyCmdTest.java index 7f420054..6196a847 100644 --- a/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/RoundTripSignVerifyCmdTest.java +++ b/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/RoundTripSignVerifyCmdTest.java @@ -189,7 +189,8 @@ public class RoundTripSignVerifyCmdTest extends CLITest { } @Test - public void testSignWithIncapableKey() throws IOException, PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException { + public void testSignWithIncapableKey() + throws IOException, PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException { PGPSecretKeyRing secretKeys = PGPainless.buildKeyRing() .addUserId("Cannot Sign ") .setPrimaryKey(KeySpec.getBuilder(KeyType.EDDSA(EdDSACurve._Ed25519), KeyFlag.CERTIFY_OTHER)) From b9f985a84c0fa5e64977173ceb2240b0901b51bd Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Fri, 18 Nov 2022 14:55:14 +0100 Subject: [PATCH 133/763] Add tests for SOP decrypt --- .../RoundTripEncryptDecryptCmdTest.java | 81 +++++++++++++++++++ 1 file changed, 81 insertions(+) diff --git a/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/RoundTripEncryptDecryptCmdTest.java b/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/RoundTripEncryptDecryptCmdTest.java index 9122829a..d314de20 100644 --- a/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/RoundTripEncryptDecryptCmdTest.java +++ b/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/RoundTripEncryptDecryptCmdTest.java @@ -563,4 +563,85 @@ public class RoundTripEncryptDecryptCmdTest extends CLITest { keyFile.getAbsolutePath())); assertEquals(msg, out.toString()); } + + @Test + public void decryptMalformedMessageYieldsBadData() throws IOException { + // Message contains encrypted data packet which contains the plaintext directly - no literal data packet. + // It is therefore malformed. + String malformed = "-----BEGIN PGP MESSAGE-----\n" + + "Version: BCPG v1.72b04\n" + + "\n" + + "hF4D831k4umlLu4SAQdApKA6VDKSLQvwS2kbWqlhcXD8XHdFkSccqv5tBptZnBgw\n" + + "nZNXVhwUpap0ymb4jPTD+EVPKOfPyy04ouIGZAJKkfYDeSL/8sKcbnPPuQJYYjGQ\n" + + "ySDNmidrtTonwcSuwAfRyn74BBqOVhrr8GXkVIfevIlZFQ==\n" + + "=wIgl\n" + + "-----END PGP MESSAGE-----"; + File key = writeFile("key.asc", KEY); + pipeStringToStdin(malformed); + int exitCode = executeCommand("decrypt", key.getAbsolutePath()); + assertEquals(SOPGPException.BadData.EXIT_CODE, exitCode); + } + + @Test + public void decryptWithPasswordWithPendingWhitespaceWorks() throws IOException { + assertEncryptWithPasswordADecryptWithPasswordBWorks("sw0rdf1sh", "sw0rdf1sh \n"); + } + + @Test + public void encryptWithTrailingWhitespaceDecryptWithoutWorks() throws IOException { + assertEncryptWithPasswordADecryptWithPasswordBWorks("sw0rdf1sh \n", "sw0rdf1sh"); + } + + @Test + public void decryptWithWhitespacePasswordWorks() throws IOException { + // is encrypted for "sw0rdf1sh \n" + String encryptedToPasswordWithTrailingWhitespace = "-----BEGIN PGP MESSAGE-----\n" + + "\n" + + "jA0ECQMC32tEJug0BCpg0kABfT3dKgA4K8XGpk2ul67BXLZD//fCCSmIQIWnNhE1\n" + + "6q97xFQ628K8f/58+XoBzLqLDT+LEz9Bz+Yg9QfzkEFy\n" + + "=2Y+K\n" + + "-----END PGP MESSAGE-----"; + pipeStringToStdin(encryptedToPasswordWithTrailingWhitespace); + File password = writeFile("password", "sw0rdf1sh \n"); + ByteArrayOutputStream plaintext = pipeStdoutToStream(); + assertSuccess(executeCommand("decrypt", "--with-password", password.getAbsolutePath())); + + assertEquals("Hello, World!\n", plaintext.toString()); + } + + private void assertEncryptWithPasswordADecryptWithPasswordBWorks(String passwordA, String passwordB) + throws IOException { + File passwordAFile = writeFile("password", passwordA); + File passwordBFile = writeFile("passwordWithWS", passwordB); + + String msg = "Hello, World!\n"; + pipeStringToStdin(msg); + ByteArrayOutputStream ciphertext = pipeStdoutToStream(); + assertSuccess(executeCommand("encrypt", "--with-password", passwordAFile.getAbsolutePath())); + + pipeStringToStdin(ciphertext.toString()); + ByteArrayOutputStream plaintext = pipeStdoutToStream(); + assertSuccess(executeCommand("decrypt", "--with-password", passwordBFile.getAbsolutePath())); + + assertEquals(msg, plaintext.toString()); + } + + @Test + public void testDecryptWithoutDecryptionOptionFails() throws IOException { + String ciphertext = "-----BEGIN PGP MESSAGE-----\n" + + "Version: PGPainless\n" + + "\n" + + "hF4D831k4umlLu4SAQdAYisjZTDRm217LHQbqjB766tm62CKTkRj3Gd0wYxVRCgw\n" + + "48SnOJINCJoPgDsxk2NiJmLCImoiET7IElqxN9htdDXQJwcRK+71r/ZyO4YJpWuX\n" + + "0sAAAcEFc3nT+un31sOi8KoBJlc5n+MemntQvcWDs8B87BEW/Ncjrs0s4pJpZKBQ\n" + + "/AWc4wLCI3ylfMQJB2pICqaOO3KP3WepgTIw5fuZm6YfriKQi7uZvVx1N+uaCIoa\n" + + "K2IVVf/7O9KZJ9GbsGYdpBj9IdaIZiVS3Xi8rwgQl3haI/EeHC3nnCsWyj23Fjt3\n" + + "LjbMqpHbSnp8U1cQ8rXavrREaKv69PFeJio6/hRg32TzJqn05dPALRxHMEkxxa4h\n" + + "FpVU\n" + + "=edS5\n" + + "-----END PGP MESSAGE-----"; + pipeStringToStdin(ciphertext); + int exitCode = executeCommand("decrypt"); + assertEquals(SOPGPException.MissingArg.EXIT_CODE, exitCode); + } } From a9014f1985c63045f58b5d5f27e25ae8825004ce Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Fri, 18 Nov 2022 15:27:58 +0100 Subject: [PATCH 134/763] Add disabled test for broken data during dearmor --- .../pgpainless/cli/commands/DearmorCmdTest.java | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/DearmorCmdTest.java b/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/DearmorCmdTest.java index 8fcf093d..64576bfe 100644 --- a/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/DearmorCmdTest.java +++ b/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/DearmorCmdTest.java @@ -13,9 +13,11 @@ import java.io.IOException; import org.bouncycastle.openpgp.PGPPublicKeyRing; import org.bouncycastle.openpgp.PGPSecretKeyRing; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.pgpainless.PGPainless; import org.slf4j.LoggerFactory; +import sop.exception.SOPGPException; public class DearmorCmdTest extends CLITest { @@ -59,6 +61,19 @@ public class DearmorCmdTest extends CLITest { assertArrayEquals(secretKey.getEncoded(), dearmored.toByteArray()); } + @Test + @Disabled("Enable with SOP-Java 4.0.7") + // TODO: Enable + public void dearmorBrokenArmoredKeyFails() throws IOException { + // contains a "-" + String invalidBase64 = "lFgEY2vOkhYJKwYBBAHaRw8BAQdAqGOtLd1tKnuwaYYcdr2/7C0cPiCCggRMKG+Wt32QQdEAAP9VaBzjk/AaAqyykZnQHmS1HByEvRLv5/4yJMSr22451BFjtBRhbGljZUBwZ3BhaW5sZXNzLm9yZ4iOBBMWCgBBBQJja86SCRCLB1F3AflTTBYhBGLp3aTyD4NB0rxLT-IsHUXcB+VNMAp4BApsBBRYCAwEABAsJCAcFFQoJCAsCmQEAACZhAP4s8hn/RBDvyLvGROOd15EYATnWlgyi+b5WXP6cELalJwD1FZy3RROhfNtZWcJPS43fG03pYNyb0NXoitIMAaXEB5xdBGNrzpISCisGAQQBl1UBBQEBB0CqCcYethOynfni8uRO+r/cZWp9hCLy8pRIExKqzcyEFAMBCAcAAP9sRRLoZkLpDaTNNrtIBovXu2ANhL8keUMWtVcuEHnkQA6iiHUEGBYKAB0FAmNrzpICngECmwwFFgIDAQAECwkIBwUVCgkICwAKCRCLB1F3AflTTBVpAP491etrjqCMWx2bBaw3K1vP0Mix6U0vF3J4kP9UeZm6owEA4kX9VAGESvLgIc7CEiswmxdWjxnLQyCRtWXfjgFmYQucWARja86SFgkrBgEEAdpHDwEBB0DBslhDpWC6CV3xJUSo071NSO5Cf4fgOwOj+QHs8mpFbwABAPkQioSydYiMi04LyfPohyrhhcdJDHallQg+jYHHUb2pEJCI1QQYFgoAfQUCY2vOkgKeAQKbAgUWAgMBAAQLCQgHBRUKCQgLXyAEGRYKAAYFAmNrzpIACgkQiHlkvEXh+f1eywEA9A2GLU9LxCJxZf2X4qcZY//YJDChIZHPnY0Vaek1DsMBAN1YILrH2rxQeCXjm4bUKfJIRrGt6ZJscwORgNI1dFQFAAoJEIsHUXcB+VNMK3gA/3vvPm57JsHA860wlB4D1II71oFNL8TFnJqTAvpSKe1AAP49S4mKB4PE0ElcDo7n+nEYt6ba8IMRDlMorsH85mUgCw=="; + pipeStringToStdin(invalidBase64); + ByteArrayOutputStream out = pipeStdoutToStream(); + int exitCode = executeCommand("dearmor"); + + assertEquals(SOPGPException.BadData.EXIT_CODE, exitCode); + assertEquals(0, out.size()); + } @Test public void dearmorCertificate() throws IOException { From ce929fd05554ca15b9ff89140bb29cbd1a4a9c80 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Fri, 18 Nov 2022 15:35:30 +0100 Subject: [PATCH 135/763] Add inline-verify test for message without verifiable signatures --- ...oundTripInlineSignInlineVerifyCmdTest.java | 35 ++++++++++++++++--- 1 file changed, 30 insertions(+), 5 deletions(-) diff --git a/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/RoundTripInlineSignInlineVerifyCmdTest.java b/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/RoundTripInlineSignInlineVerifyCmdTest.java index 5d635beb..ce0a7674 100644 --- a/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/RoundTripInlineSignInlineVerifyCmdTest.java +++ b/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/RoundTripInlineSignInlineVerifyCmdTest.java @@ -4,16 +4,17 @@ package org.pgpainless.cli.commands; -import org.junit.jupiter.api.Test; -import org.slf4j.LoggerFactory; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.IOException; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertTrue; +import org.junit.jupiter.api.Test; +import org.slf4j.LoggerFactory; +import sop.exception.SOPGPException; public class RoundTripInlineSignInlineVerifyCmdTest extends CLITest { @@ -171,6 +172,30 @@ public class RoundTripInlineSignInlineVerifyCmdTest extends CLITest { assertTrue(verificationString.contains(CERT_1_SIGNING_KEY)); } + @Test + public void createSignedMessageWithKeyAAndVerifyWithKeyBFails() throws IOException { + File key = writeFile("key.asc", KEY_1); + File password = writeFile("password", KEY_1_PASSWORD); + File cert = writeFile("cert.asc", CERT_2); // mismatch + + pipeStringToStdin(MESSAGE); + ByteArrayOutputStream ciphertextOut = pipeStdoutToStream(); + assertSuccess(executeCommand("inline-sign", + key.getAbsolutePath(), + "--with-key-password", password.getAbsolutePath())); + + File verifications = nonExistentFile("verifications"); + pipeStringToStdin(ciphertextOut.toString()); + ByteArrayOutputStream plaintextOut = pipeStdoutToStream(); + int exitCode = executeCommand("inline-verify", + "--verifications-out", verifications.getAbsolutePath(), + cert.getAbsolutePath()); + + assertEquals(SOPGPException.NoSignature.EXIT_CODE, exitCode); + assertEquals(MESSAGE, plaintextOut.toString()); // message is emitted nonetheless + assertFalse(verifications.exists(), "Verifications file MUST NOT be written."); + } + @Test public void createAndVerifyMultiKeyBinarySignedMessage() throws IOException { File key1Pass = writeFile("password", KEY_1_PASSWORD); From 508864c4ff21d7db8ccf2e2c7fe7f915fe433a06 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Fri, 18 Nov 2022 15:36:58 +0100 Subject: [PATCH 136/763] Add test for inline-sign --as=text --- ...oundTripInlineSignInlineVerifyCmdTest.java | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/RoundTripInlineSignInlineVerifyCmdTest.java b/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/RoundTripInlineSignInlineVerifyCmdTest.java index ce0a7674..1ff3a14b 100644 --- a/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/RoundTripInlineSignInlineVerifyCmdTest.java +++ b/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/RoundTripInlineSignInlineVerifyCmdTest.java @@ -172,6 +172,31 @@ public class RoundTripInlineSignInlineVerifyCmdTest extends CLITest { assertTrue(verificationString.contains(CERT_1_SIGNING_KEY)); } + @Test + public void createAndVerifyTextSignedMessage() throws IOException { + File key = writeFile("key.asc", KEY_1); + File password = writeFile("password", KEY_1_PASSWORD); + + pipeStringToStdin(MESSAGE); + ByteArrayOutputStream ciphertextOut = pipeStdoutToStream(); + assertSuccess(executeCommand("inline-sign", + "--as", "text", + key.getAbsolutePath(), + "--with-key-password", password.getAbsolutePath())); + + File cert = writeFile("cert.asc", CERT_1); + File verifications = nonExistentFile("verifications"); + pipeStringToStdin(ciphertextOut.toString()); + ByteArrayOutputStream plaintextOut = pipeStdoutToStream(); + assertSuccess(executeCommand("inline-verify", + "--verifications-out", verifications.getAbsolutePath(), + cert.getAbsolutePath())); + + assertEquals(MESSAGE, plaintextOut.toString()); + String verificationString = readStringFromFile(verifications); + assertTrue(verificationString.contains(CERT_1_SIGNING_KEY)); + } + @Test public void createSignedMessageWithKeyAAndVerifyWithKeyBFails() throws IOException { File key = writeFile("key.asc", KEY_1); From 75c39c2fde55fd0744c9c0d070e9cb7d36ff0bdf Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Fri, 18 Nov 2022 16:03:20 +0100 Subject: [PATCH 137/763] Add tests for inline-verify --- ...oundTripInlineSignInlineVerifyCmdTest.java | 100 ++++++++++++++++++ 1 file changed, 100 insertions(+) diff --git a/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/RoundTripInlineSignInlineVerifyCmdTest.java b/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/RoundTripInlineSignInlineVerifyCmdTest.java index 1ff3a14b..d5a94f3a 100644 --- a/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/RoundTripInlineSignInlineVerifyCmdTest.java +++ b/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/RoundTripInlineSignInlineVerifyCmdTest.java @@ -11,8 +11,19 @@ import static org.junit.jupiter.api.Assertions.assertTrue; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.IOException; +import java.nio.charset.StandardCharsets; +import org.bouncycastle.bcpg.ArmoredOutputStream; +import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.PGPSecretKeyRing; +import org.bouncycastle.openpgp.PGPSignature; import org.junit.jupiter.api.Test; +import org.pgpainless.PGPainless; +import org.pgpainless.algorithm.CompressionAlgorithm; +import org.pgpainless.encryption_signing.EncryptionStream; +import org.pgpainless.encryption_signing.ProducerOptions; +import org.pgpainless.encryption_signing.SigningOptions; +import org.pgpainless.key.protection.SecretKeyRingProtector; import org.slf4j.LoggerFactory; import sop.exception.SOPGPException; @@ -311,4 +322,93 @@ public class RoundTripInlineSignInlineVerifyCmdTest extends CLITest { String verificationString = readStringFromFile(verificationsFile); assertTrue(verificationString.contains(CERT_1_SIGNING_KEY)); } + + @Test + public void cannotVerifyEncryptedMessage() throws IOException { + File key = writeFile("key.asc", KEY_2); + File cert = writeFile("cert.asc", CERT_2); + + String msg = "Hello, World!\n"; + pipeStringToStdin(msg); + ByteArrayOutputStream ciphertext = pipeStdoutToStream(); + assertSuccess(executeCommand("encrypt", cert.getAbsolutePath(), + "--sign-with", key.getAbsolutePath())); + + File verifications = nonExistentFile("verifications"); + pipeBytesToStdin(ciphertext.toByteArray()); + ByteArrayOutputStream out = pipeStdoutToStream(); + int exitCode = executeCommand("inline-verify", cert.getAbsolutePath(), + "--verifications-out", verifications.getAbsolutePath()); + + assertEquals(SOPGPException.BadData.EXIT_CODE, exitCode); + assertEquals(0, out.size()); + } + + @Test + public void createMalformedMessage() throws IOException, PGPException { + String msg = "Hello, World!\n"; + PGPSecretKeyRing key = PGPainless.readKeyRing().secretKeyRing(KEY_2); + ByteArrayOutputStream ciphertext = new ByteArrayOutputStream(); + EncryptionStream encryptionStream = PGPainless.encryptAndOrSign() + .onOutputStream(ciphertext) + .withOptions(ProducerOptions.sign(SigningOptions.get() + .addDetachedSignature(SecretKeyRingProtector.unprotectedKeys(), key) + ).overrideCompressionAlgorithm(CompressionAlgorithm.UNCOMPRESSED) + .setAsciiArmor(false)); + encryptionStream.write(msg.getBytes(StandardCharsets.UTF_8)); + encryptionStream.close(); + PGPSignature sig = encryptionStream.getResult().getDetachedSignatures().entrySet() + .iterator().next().getValue().iterator().next(); + ArmoredOutputStream armorOut = new ArmoredOutputStream(System.out); + armorOut.write(ciphertext.toByteArray()); + armorOut.write(sig.getEncoded()); + armorOut.close(); + } + + @Test + public void cannotVerifyMalformedMessage() throws IOException { + // appended signature -> malformed + String malformedSignedMessage = "-----BEGIN PGP MESSAGE-----\n" + + "Version: BCPG v1.72b04\n" + + "\n" + + "yxRiAAAAAABIZWxsbywgV29ybGQhCoh1BAAWCgAnBQJjd52aCRCPvdNtAYMWcxYh\n" + + "BHoHPt8nPJAnltJZUo+9020BgxZzAACThwD/Vr7CMitMOul40VK12XXjOv5f8vgi\n" + + "ksqhrI2ysItID9oA/0Csgf3Sv2YenYVzqnd0hhiPe5IVPl8w4sTZKpriYMIG\n" + + "=DPPU\n" + + "-----END PGP MESSAGE-----"; + File cert = writeFile("cert.asc", CERT_2); + File verifications = nonExistentFile("verifications"); + + pipeStringToStdin(malformedSignedMessage); + ByteArrayOutputStream out = pipeStdoutToStream(); + int exitCode = executeCommand("inline-verify", cert.getAbsolutePath(), + "--verifications-out", verifications.getAbsolutePath()); + + assertEquals(SOPGPException.BadData.EXIT_CODE, exitCode); + assertEquals("Hello, World!\n", out.toString()); + } + + @Test + public void verifyPrependedSignedMessage() throws IOException { + // message with prepended signature + String malformedSignedMessage = "-----BEGIN PGP SIGNATURE-----\n" + + "Version: BCPG v1.72b04\n" + + "\n" + + "iHUEABYKACcFAmN3nOUJEI+9020BgxZzFiEEegc+3yc8kCeW0llSj73TbQGDFnMA\n" + + "ANPKAPkBxLVHvgeCkX/tTHdBH3CDeuUQF2wmtUmGXqhZA1IFtwD/dK0XQBHO3RO+\n" + + "GHpzA7fDAroqF0zM72tu2W4PPw04FgKjATstksQAAh6pOTn5Ogrh+UU5KYpcAA==\n" + + "=xtik\n" + + "-----END PGP SIGNATURE-----"; + File cert = writeFile("cert.asc", CERT_2); + File verifications = nonExistentFile("verifications"); + + pipeStringToStdin(malformedSignedMessage); + ByteArrayOutputStream out = pipeStdoutToStream(); + assertSuccess(executeCommand("inline-verify", cert.getAbsolutePath(), + "--verifications-out", verifications.getAbsolutePath())); + assertEquals("Hello, World!\n", out.toString()); + String ver = readStringFromFile(verifications); + assertEquals( + "2022-11-18T14:55:33Z 7A073EDF273C902796D259528FBDD36D01831673 AEA0FD2C899D3FC077815F0026560D2AE53DB86F\n", ver); + } } From a34f46b6c6b64949ffa2525bcd40760ae513ef96 Mon Sep 17 00:00:00 2001 From: GregorGott <85785843+GregorGott@users.noreply.github.com> Date: Sun, 20 Nov 2022 10:54:11 +0000 Subject: [PATCH 138/763] Correct method name MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Correct `verifyWith()` to `verifyWithCert()´ --- docs/source/pgpainless-sop/quickstart.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/pgpainless-sop/quickstart.md b/docs/source/pgpainless-sop/quickstart.md index ecd7d81d..10ef0a72 100644 --- a/docs/source/pgpainless-sop/quickstart.md +++ b/docs/source/pgpainless-sop/quickstart.md @@ -209,7 +209,7 @@ byte[] ciphertext = ...; // the encrypted message ReadyWithResult readyWithResult = sop.decrypt() .withKey(bobKey) - .verifyWith(aliceCert) + .verifyWithCert(aliceCert) .withKeyPassword("password123") // if decryption key is protected .ciphertext(ciphertext); ``` From ab82a638ccbe4de4ce0dca2c7049ef4944ef6c0e Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 21 Nov 2022 14:11:12 +0100 Subject: [PATCH 139/763] Add tests for inline-sign --- ...oundTripInlineSignInlineVerifyCmdTest.java | 48 +++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/RoundTripInlineSignInlineVerifyCmdTest.java b/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/RoundTripInlineSignInlineVerifyCmdTest.java index d5a94f3a..1e5310f5 100644 --- a/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/RoundTripInlineSignInlineVerifyCmdTest.java +++ b/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/RoundTripInlineSignInlineVerifyCmdTest.java @@ -411,4 +411,52 @@ public class RoundTripInlineSignInlineVerifyCmdTest extends CLITest { assertEquals( "2022-11-18T14:55:33Z 7A073EDF273C902796D259528FBDD36D01831673 AEA0FD2C899D3FC077815F0026560D2AE53DB86F\n", ver); } + + @Test + public void testInlineSignWithMissingSecretKeysFails() throws IOException { + String missingSecretKeys = "-----BEGIN PGP PRIVATE KEY BLOCK-----\n" + + "Comment: 8677 37CA 1979 28FA 325A DE56 B455 9329 9882 36BE\n" + + "Comment: Mrs. Secret Key \n" + + "\n" + + "lEwEY3t3pRYJKwYBBAHaRw8BAQdA7lifUc85s7omw7eYNIaIj2mZrGeZ9KkG0WX2\n" + + "hAx5qXT+AGUAR05VAhAAAAAAAAAAAAAAAAAAAAAAtCFNcnMuIFNlY3JldCBLZXkg\n" + + "PG1pc3NAc2VjcmV0LmtleT6IjwQTFgoAQQUCY3t3pQkQtFWTKZiCNr4WIQSGdzfK\n" + + "GXko+jJa3la0VZMpmII2vgKeAQKbAQUWAgMBAAQLCQgHBRUKCQgLApkBAABNTQEA\n" + + "uU5L9hJ1QKWxL5wetJwR08rXJTzsuX1LRfy8dlnlJl0BAKPSqydLoTEVlJQ/2sjO\n" + + "xQmc6aedoOoXKKVNDW5ibrsEnFEEY3t3pRIKKwYBBAGXVQEFAQEHQA/WdwR+NFaY\n" + + "7NeZnRwI3X9sI5fMq0vtEauMLfZjqTc/AwEIB/4AZQBHTlUCEAAAAAAAAAAAAAAA\n" + + "AAAAAACIdQQYFgoAHQUCY3t3pQKeAQKbDAUWAgMBAAQLCQgHBRUKCQgLAAoJELRV\n" + + "kymYgja+8XMA/1quBVvaSf4QxbB2S7rKt93rAynDLqGQD8hC6wiZc+ihAQC87n2r\n" + + "meZ9kiYLYiQuBTGvXyzDBtw5m7wQtMWTfXisBpxMBGN7d6UWCSsGAQQB2kcPAQEH\n" + + "QMguDhFon0ZI//CIpC2ZndmtvKdJhcEAeVNkdcsIZajl/gBlAEdOVQIQAAAAAAAA\n" + + "AAAAAAAAAAAAAIjVBBgWCgB9BQJje3elAp4BApsCBRYCAwEABAsJCAcFFQoJCAtf\n" + + "IAQZFgoABgUCY3t3pQAKCRC14KclsvqqOstPAQDYiL7+4HucWKmd7dcd9XJZpdB6\n" + + "lneoK0qku0wvTVjX7gEAtUt2eXMlBE4ox+ZmY964PCc2gEHuC7PBtsAzuF7GSQwA\n" + + "CgkQtFWTKZiCNr7JKwEA3aLsOWAYzqvKgiboYSzle+SVBUb3chKlzf3YmckjmwgA\n" + + "/3YN1W8CiQFvE9NvetZkr2wXB+QVkuL6cxM0ogEo4lAG\n" + + "=9ZMl\n" + + "-----END PGP PRIVATE KEY BLOCK-----\n"; + File key = writeFile("key.asc", missingSecretKeys); + + pipeStringToStdin("Hello, World!\n"); + ByteArrayOutputStream out = pipeStdoutToStream(); + int exitCode = executeCommand("inline-sign", key.getAbsolutePath()); + + assertEquals(SOPGPException.KeyCannotSign.EXIT_CODE, exitCode); + assertEquals(0, out.size()); + } + + @Test + public void signWithProtectedKeyWithWrongPassphraseFails() throws IOException { + File key = writeFile("key.asc", KEY_1); + File password = writeFile("password.asc", "not_correct!"); + + pipeStringToStdin("Hello, World!\n"); + ByteArrayOutputStream out = pipeStdoutToStream(); + int exitCode = executeCommand("inline-sign", key.getAbsolutePath(), + "--with-key-password", password.getAbsolutePath()); + + assertEquals(SOPGPException.KeyIsProtected.EXIT_CODE, exitCode); + assertEquals(0, out.size()); + } } From a19fc9ebda01cd694c9e1a50a1d4327dc1d0e556 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 21 Nov 2022 15:04:13 +0100 Subject: [PATCH 140/763] Add tests for inline-detach --- .../cli/commands/InlineDetachCmdTest.java | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/InlineDetachCmdTest.java b/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/InlineDetachCmdTest.java index eabdd6ad..8854d837 100644 --- a/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/InlineDetachCmdTest.java +++ b/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/InlineDetachCmdTest.java @@ -129,4 +129,27 @@ public class InlineDetachCmdTest extends CLITest { assertEquals(0, msgOut.size()); } + @Test + public void detachNonOpenPgpDataFails() throws IOException { + File sig = nonExistentFile("sig.asc"); + pipeStringToStdin("This is non-OpenPGP data and therefore we cannot detach any signatures from it."); + int exitCode = executeCommand("inline-detach", "--signatures-out", sig.getAbsolutePath()); + + assertEquals(SOPGPException.BadData.EXIT_CODE, exitCode); + } + + @Test + public void detachMissingSignaturesFromCleartextSignedMessageFails() throws IOException { + String cleartextSignedNoSigs = "-----BEGIN PGP SIGNED MESSAGE-----\n" + + "\n" + + "Hello, World!\n" + + "What's Up!??\n" + + "\n" + + "\n"; + pipeStringToStdin(cleartextSignedNoSigs); + File sig = nonExistentFile("sig.asc"); + int exitCode = executeCommand("inline-detach", "--signatures-out", sig.getAbsolutePath()); + + assertEquals(SOPGPException.BadData.EXIT_CODE, exitCode); + } } From e4560ac5b502334d0bd6f50122517c59eccb8850 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 21 Nov 2022 15:04:52 +0100 Subject: [PATCH 141/763] Cleartext Signaure Framework: Support for multiple Hash: headers --- .../encryption_signing/EncryptionStream.java | 27 ++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) 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 27d35ea5..2e370b94 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 @@ -8,7 +8,10 @@ import java.io.BufferedOutputStream; 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.Set; import javax.annotation.Nonnull; import org.bouncycastle.bcpg.ArmoredOutputStream; @@ -22,6 +25,7 @@ import org.bouncycastle.openpgp.PGPSignatureGenerator; import org.bouncycastle.openpgp.operator.PGPDataEncryptorBuilder; import org.bouncycastle.openpgp.operator.PGPKeyEncryptionMethodGenerator; import org.pgpainless.algorithm.CompressionAlgorithm; +import org.pgpainless.algorithm.HashAlgorithm; import org.pgpainless.algorithm.StreamEncoding; import org.pgpainless.algorithm.SymmetricKeyAlgorithm; import org.pgpainless.implementation.ImplementationFactory; @@ -165,9 +169,8 @@ public final class EncryptionStream extends OutputStream { private void prepareLiteralDataProcessing() throws IOException { if (options.isCleartextSigned()) { - // Begin cleartext with hash algorithm of first signing method - SigningOptions.SigningMethod firstMethod = options.getSigningOptions().getSigningMethods().values().iterator().next(); - armorOutputStream.beginClearText(firstMethod.getHashAlgorithm().getAlgorithmId()); + int[] algorithmIds = collectHashAlgorithmsForCleartextSigning(); + armorOutputStream.beginClearText(algorithmIds); return; } @@ -195,6 +198,24 @@ public final class EncryptionStream extends OutputStream { outermostStream = crlfGeneratorStream; } + private int[] collectHashAlgorithmsForCleartextSigning() { + SigningOptions signOpts = options.getSigningOptions(); + Set hashAlgorithms = new HashSet<>(); + if (signOpts != null) { + for (SigningOptions.SigningMethod method : signOpts.getSigningMethods().values()) { + hashAlgorithms.add(method.getHashAlgorithm()); + } + } + + int[] algorithmIds = new int[hashAlgorithms.size()]; + Iterator iterator = hashAlgorithms.iterator(); + for (int i = 0; i < algorithmIds.length; i++) { + algorithmIds[i] = iterator.next().getAlgorithmId(); + } + + return algorithmIds; + } + @Override public void write(int data) throws IOException { outermostStream.write(data); From 24ec665f76a66d7e310c9c9e74d403f2c09a8ef6 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 22 Nov 2022 14:39:32 +0100 Subject: [PATCH 142/763] Bump bcpg to 1.72.3 --- version.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.gradle b/version.gradle index eb20dc66..86fc8f54 100644 --- a/version.gradle +++ b/version.gradle @@ -13,7 +13,7 @@ allprojects { // unfortunately we rely on 1.72.1 or 1.72.3 for a patch for https://github.com/bcgit/bc-java/issues/1257 // which is a bug we introduced with a PR against BC :/ oops // When bouncyCastleVersion is 1.71, bouncyPgVersion can simply be set to 1.71 as well. - bouncyPgVersion = '1.72.1' + bouncyPgVersion = '1.72.3' junitVersion = '5.8.2' logbackVersion = '1.2.11' mockitoVersion = '4.5.1' From 616e14d04354d072413feef1dbcd8238aa69038d Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 22 Nov 2022 14:40:31 +0100 Subject: [PATCH 143/763] Enable tests for unsupported s2k identifiers --- .../decryption_verification/UnsupportedPacketVersionsTest.java | 3 --- 1 file changed, 3 deletions(-) diff --git a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/UnsupportedPacketVersionsTest.java b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/UnsupportedPacketVersionsTest.java index 68916348..f8ce9e29 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/UnsupportedPacketVersionsTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/UnsupportedPacketVersionsTest.java @@ -15,7 +15,6 @@ import org.bouncycastle.openpgp.PGPException; import org.bouncycastle.openpgp.PGPPublicKeyRing; import org.bouncycastle.openpgp.PGPSecretKeyRing; import org.bouncycastle.util.io.Streams; -import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.pgpainless.PGPainless; @@ -361,13 +360,11 @@ public class UnsupportedPacketVersionsTest { } @Test - @Disabled("Enable once https://github.com/bcgit/bc-java/pull/1268 is available") public void pkesk3_skesk4Ws2k23_seip() throws PGPException, IOException { decryptAndCompare(PKESK3_SKESK4wS2K23_SEIP, "Encrypted using SEIP + MDC."); } @Test - @Disabled("Enable once https://github.com/bcgit/bc-java/pull/1268 is available") public void skesk4Ws2k23_pkesk3_seip() throws PGPException, IOException { decryptAndCompare(SKESK4wS2K23_PKESK3_SEIP, "Encrypted using SEIP + MDC."); } From 3ae2afcfa0155ba6439a1ebd74b4a160d2df0323 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 22 Nov 2022 14:46:08 +0100 Subject: [PATCH 144/763] Update changelog --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6f19bc95..9fc7e655 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ SPDX-License-Identifier: CC0-1.0 # PGPainless Changelog ## 1.4.0-rc2-SNAPSHOT +- Bump `bcpg-jdk15to18` to `1.72.3` - Use BCs `PGPEncryptedDataList.extractSessionKeyEncryptedData()` method to do decryption using session keys. This enables decryption of messages without encrypted session key packets. @@ -13,6 +14,8 @@ SPDX-License-Identifier: CC0-1.0 - Depend on `pgp-certificate-store` - Add `ConsumerOptions.addVerificationCerts(PGPCertificateStore)` to allow sourcing certificates from e.g. a [certificate store implementation](https://github.com/pgpainless/cert-d-java). +- Make `DecryptionStream.getMetadata()` first class + - Deprecate `DecryptionStream.getResult()` ## 1.4.0-rc1 - Reimplement message consumption via new `OpenPgpMessageInputStream` From 39f8f89fe0bb117f91655ef21440042b7f36a8f1 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 22 Nov 2022 15:19:39 +0100 Subject: [PATCH 145/763] Add convenience methods to MessageMetadata --- .../MessageMetadata.java | 221 +++++++++++------- 1 file changed, 139 insertions(+), 82 deletions(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/MessageMetadata.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/MessageMetadata.java index 3af2a4d3..5318fcb1 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/MessageMetadata.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/MessageMetadata.java @@ -4,6 +4,18 @@ package org.pgpainless.decryption_verification; +import java.util.ArrayList; +import java.util.Date; +import java.util.Iterator; +import java.util.List; +import java.util.NoSuchElementException; +import java.util.function.Function; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +import org.bouncycastle.openpgp.PGPKeyRing; +import org.bouncycastle.openpgp.PGPLiteralData; +import org.bouncycastle.openpgp.PGPPublicKey; import org.pgpainless.algorithm.CompressionAlgorithm; import org.pgpainless.algorithm.StreamEncoding; import org.pgpainless.algorithm.SymmetricKeyAlgorithm; @@ -11,14 +23,6 @@ import org.pgpainless.exception.MalformedOpenPgpMessageException; import org.pgpainless.key.SubkeyIdentifier; import org.pgpainless.util.SessionKey; -import javax.annotation.Nonnull; -import javax.annotation.Nullable; -import java.util.ArrayList; -import java.util.Date; -import java.util.Iterator; -import java.util.List; -import java.util.NoSuchElementException; - /** * View for extracting metadata about a {@link Message}. */ @@ -40,18 +44,9 @@ public class MessageMetadata { public @Nonnull OpenPgpMetadata toLegacyMetadata() { OpenPgpMetadata.Builder resultBuilder = OpenPgpMetadata.getBuilder(); resultBuilder.setCompressionAlgorithm(getCompressionAlgorithm()); - Date modDate = getModificationDate(); - if (modDate != null) { - resultBuilder.setModificationDate(modDate); - } - String fileName = getFilename(); - if (fileName != null) { - resultBuilder.setFileName(fileName); - } - StreamEncoding encoding = getFormat(); - if (encoding != null) { - resultBuilder.setFileEncoding(encoding); - } + resultBuilder.setModificationDate(getModificationDate()); + resultBuilder.setFileName(getFilename()); + resultBuilder.setFileEncoding(getLiteralDataEncoding()); resultBuilder.setSessionKey(getSessionKey()); resultBuilder.setDecryptionKey(getDecryptionKey()); @@ -75,6 +70,39 @@ public class MessageMetadata { return resultBuilder.build(); } + public boolean isEncrypted() { + SymmetricKeyAlgorithm algorithm = getEncryptionAlgorithm(); + return algorithm != null && algorithm != SymmetricKeyAlgorithm.NULL; + } + + public boolean isEncryptedFor(@Nonnull PGPKeyRing keys) { + Iterator encryptionLayers = getEncryptionLayers(); + while (encryptionLayers.hasNext()) { + EncryptedData encryptedData = encryptionLayers.next(); + for (long recipient : encryptedData.getRecipients()) { + PGPPublicKey key = keys.getPublicKey(recipient); + if (key != null) { + return true; + } + } + } + return false; + } + + public @Nonnull Iterator getEncryptionLayers() { + return new LayerIterator(message) { + @Override + public boolean matches(Nested layer) { + return layer instanceof EncryptedData; + } + + @Override + public EncryptedData getProperty(Layer last) { + return (EncryptedData) last; + } + }; + } + /** * Return the {@link SymmetricKeyAlgorithm} of the outermost encrypted data packet, or null if message is * unencrypted. @@ -82,11 +110,7 @@ public class MessageMetadata { * @return encryption algorithm */ public @Nullable SymmetricKeyAlgorithm getEncryptionAlgorithm() { - Iterator algorithms = getEncryptionAlgorithms(); - if (algorithms.hasNext()) { - return algorithms.next(); - } - return null; + return firstOrNull(getEncryptionAlgorithms()); } /** @@ -98,15 +122,19 @@ public class MessageMetadata { * @return iterator of symmetric encryption algorithms */ public @Nonnull Iterator getEncryptionAlgorithms() { - return new LayerIterator(message) { + return map(getEncryptionLayers(), encryptedData -> encryptedData.algorithm); + } + + public @Nonnull Iterator getCompressionLayers() { + return new LayerIterator(message) { @Override - public boolean matches(Nested layer) { - return layer instanceof EncryptedData; + boolean matches(Layer layer) { + return layer instanceof CompressedData; } @Override - public SymmetricKeyAlgorithm getProperty(Layer last) { - return ((EncryptedData) last).algorithm; + CompressedData getProperty(Layer last) { + return (CompressedData) last; } }; } @@ -118,11 +146,7 @@ public class MessageMetadata { * @return compression algorithm */ public @Nullable CompressionAlgorithm getCompressionAlgorithm() { - Iterator algorithms = getCompressionAlgorithms(); - if (algorithms.hasNext()) { - return algorithms.next(); - } - return null; + return firstOrNull(getCompressionAlgorithms()); } /** @@ -134,17 +158,7 @@ public class MessageMetadata { * @return iterator of compression algorithms */ public @Nonnull Iterator getCompressionAlgorithms() { - return new LayerIterator(message) { - @Override - public boolean matches(Nested layer) { - return layer instanceof CompressedData; - } - - @Override - public CompressionAlgorithm getProperty(Layer last) { - return ((CompressedData) last).algorithm; - } - }; + return map(getCompressionLayers(), compressionLayer -> compressionLayer.algorithm); } /** @@ -154,11 +168,7 @@ public class MessageMetadata { * @return session key of the message */ public @Nullable SessionKey getSessionKey() { - Iterator sessionKeys = getSessionKeys(); - if (sessionKeys.hasNext()) { - return sessionKeys.next(); - } - return null; + return firstOrNull(getSessionKeys()); } /** @@ -170,17 +180,15 @@ public class MessageMetadata { * @return iterator of session keys */ public @Nonnull Iterator getSessionKeys() { - return new LayerIterator(message) { - @Override - boolean matches(Nested layer) { - return layer instanceof EncryptedData; - } + return map(getEncryptionLayers(), encryptedData -> encryptedData.sessionKey); + } - @Override - SessionKey getProperty(Layer last) { - return ((EncryptedData) last).getSessionKey(); - } - }; + public boolean isVerifiedSignedBy(@Nonnull PGPKeyRing keys) { + return isVerifiedInlineSignedBy(keys) || isVerifiedDetachedSignedBy(keys); + } + + public boolean isVerifiedDetachedSignedBy(@Nonnull PGPKeyRing keys) { + return containsSignatureBy(getVerifiedDetachedSignatures(), keys); } /** @@ -202,6 +210,10 @@ public class MessageMetadata { return message.getRejectedDetachedSignatures(); } + public boolean isVerifiedInlineSignedBy(@Nonnull PGPKeyRing keys) { + return containsSignatureBy(getVerifiedInlineSignatures(), keys); + } + /** * Return a list of all verified inline-signatures. * This list contains all acceptable, correct signatures that were part of the message itself. @@ -291,6 +303,28 @@ public class MessageMetadata { }; } + private static boolean containsSignatureBy(@Nonnull List verifications, + @Nonnull PGPKeyRing keys) { + for (SignatureVerification verification : verifications) { + SubkeyIdentifier issuer = verification.getSigningKey(); + if (issuer == null) { + // No issuer, shouldn't happen, but better be safe and skip... + continue; + } + + if (keys.getPublicKey().getKeyID() != issuer.getPrimaryKeyId()) { + // Wrong cert + continue; + } + + if (keys.getPublicKey(issuer.getSubkeyId()) != null) { + // Matching cert and signing key + return true; + } + } + return false; + } + /** * Return the value of the literal data packet's filename field. * This value can be used to store a decrypted file under its original filename, @@ -300,14 +334,23 @@ public class MessageMetadata { * @return filename * @see RFC4880 §5.9. Literal Data Packet */ - public @Nullable String getFilename() { + public @Nonnull String getFilename() { LiteralData literalData = findLiteralData(); if (literalData == null) { - return null; + throw new NoSuchElementException("No Literal Data Packet found."); } return literalData.getFileName(); } + /** + * Returns true, if the filename of the literal data packet indicates that the data is intended for your eyes only. + * + * @return isForYourEyesOnly + */ + public boolean isForYourEyesOnly() { + return PGPLiteralData.CONSOLE.equals(getFilename()); + } + /** * Return the value of the literal data packets modification date field. * This value can be used to restore the modification date of a decrypted file, @@ -316,10 +359,10 @@ public class MessageMetadata { * @return modification date * @see RFC4880 §5.9. Literal Data Packet */ - public @Nullable Date getModificationDate() { + public @Nonnull Date getModificationDate() { LiteralData literalData = findLiteralData(); if (literalData == null) { - return null; + throw new NoSuchElementException("No Literal Data Packet found."); } return literalData.getModificationDate(); } @@ -332,10 +375,10 @@ public class MessageMetadata { * @return format * @see RFC4880 §5.9. Literal Data Packet */ - public @Nullable StreamEncoding getFormat() { + public @Nonnull StreamEncoding getLiteralDataEncoding() { LiteralData literalData = findLiteralData(); if (literalData == null) { - return null; + throw new NoSuchElementException("No Literal Data Packet found."); } return literalData.getFormat(); } @@ -368,21 +411,7 @@ public class MessageMetadata { * @return decryption key */ public SubkeyIdentifier getDecryptionKey() { - Iterator iterator = new LayerIterator(message) { - @Override - public boolean matches(Nested layer) { - return layer instanceof EncryptedData; - } - - @Override - public SubkeyIdentifier getProperty(Layer last) { - return ((EncryptedData) last).decryptionKey; - } - }; - if (iterator.hasNext()) { - return iterator.next(); - } - return null; + return firstOrNull(map(getEncryptionLayers(), encryptedData -> encryptedData.decryptionKey)); } public abstract static class Layer { @@ -744,4 +773,32 @@ public class MessageMetadata { abstract O getProperty(Layer last); } + + private static Iterator map(Iterator from, Function mapping) { + return new Iterator() { + @Override + public boolean hasNext() { + return from.hasNext(); + } + + @Override + public B next() { + return mapping.apply(from.next()); + } + }; + } + + private static @Nullable A firstOrNull(Iterator iterator) { + if (iterator.hasNext()) { + return iterator.next(); + } + return null; + } + + private static @Nonnull A firstOr(Iterator iterator, A item) { + if (iterator.hasNext()) { + return iterator.next(); + } + return item; + } } From 8f6227c14b359792fe9d03ef3b3b938d9e1e9718 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 22 Nov 2022 15:20:06 +0100 Subject: [PATCH 146/763] Rework some tests to use MessageMetadata --- .../MessageMetadataTest.java | 4 ++-- .../OpenPgpMessageInputStreamTest.java | 2 +- .../FileInformationTest.java | 20 +++++++++---------- 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/MessageMetadataTest.java b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/MessageMetadataTest.java index d87fc6bf..771cb8f1 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/MessageMetadataTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/MessageMetadataTest.java @@ -62,7 +62,7 @@ public class MessageMetadataTest { assertEquals("", metadata.getFilename()); JUtils.assertDateEquals(new Date(0L), metadata.getModificationDate()); - assertEquals(StreamEncoding.BINARY, metadata.getFormat()); + assertEquals(StreamEncoding.BINARY, metadata.getLiteralDataEncoding()); } @Test @@ -79,6 +79,6 @@ public class MessageMetadataTest { assertNull(metadata.getEncryptionAlgorithm()); assertEquals("collateral_murder.zip", metadata.getFilename()); assertEquals(DateUtil.parseUTCDate("2010-04-05 10:12:03 UTC"), metadata.getModificationDate()); - assertEquals(StreamEncoding.BINARY, metadata.getFormat()); + assertEquals(StreamEncoding.BINARY, metadata.getLiteralDataEncoding()); } } diff --git a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStreamTest.java b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStreamTest.java index d2c62fb2..01966bbe 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStreamTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStreamTest.java @@ -335,7 +335,7 @@ public class OpenPgpMessageInputStreamTest { assertNull(metadata.getEncryptionAlgorithm()); assertEquals("", metadata.getFilename()); JUtils.assertDateEquals(new Date(0L), metadata.getModificationDate()); - assertEquals(StreamEncoding.BINARY, metadata.getFormat()); + assertEquals(StreamEncoding.BINARY, metadata.getLiteralDataEncoding()); assertTrue(metadata.getVerifiedInlineSignatures().isEmpty()); assertTrue(metadata.getRejectedInlineSignatures().isEmpty()); } diff --git a/pgpainless-core/src/test/java/org/pgpainless/encryption_signing/FileInformationTest.java b/pgpainless-core/src/test/java/org/pgpainless/encryption_signing/FileInformationTest.java index 662971e4..2b0a7106 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/encryption_signing/FileInformationTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/encryption_signing/FileInformationTest.java @@ -28,7 +28,7 @@ import org.pgpainless.PGPainless; import org.pgpainless.algorithm.StreamEncoding; import org.pgpainless.decryption_verification.ConsumerOptions; import org.pgpainless.decryption_verification.DecryptionStream; -import org.pgpainless.decryption_verification.OpenPgpMetadata; +import org.pgpainless.decryption_verification.MessageMetadata; public class FileInformationTest { @@ -80,11 +80,11 @@ public class FileInformationTest { decryptionStream.close(); - OpenPgpMetadata decResult = decryptionStream.getResult(); + MessageMetadata decResult = decryptionStream.getMetadata(); - assertEquals(fileName, decResult.getFileName()); + assertEquals(fileName, decResult.getFilename()); JUtils.assertDateEquals(modificationDate, decResult.getModificationDate()); - assertEquals(encoding, decResult.getFileEncoding()); + assertEquals(encoding, decResult.getLiteralDataEncoding()); } @Test @@ -119,11 +119,11 @@ public class FileInformationTest { decryptionStream.close(); - OpenPgpMetadata decResult = decryptionStream.getResult(); + MessageMetadata decResult = decryptionStream.getMetadata(); - assertEquals("", decResult.getFileName()); + assertEquals("", decResult.getFilename()); JUtils.assertDateEquals(PGPLiteralData.NOW, decResult.getModificationDate()); - assertEquals(PGPLiteralData.BINARY, decResult.getFileEncoding().getCode()); + assertEquals(PGPLiteralData.BINARY, decResult.getLiteralDataEncoding().getCode()); assertFalse(decResult.isForYourEyesOnly()); } @@ -160,11 +160,11 @@ public class FileInformationTest { decryptionStream.close(); - OpenPgpMetadata decResult = decryptionStream.getResult(); + MessageMetadata decResult = decryptionStream.getMetadata(); - assertEquals(PGPLiteralData.CONSOLE, decResult.getFileName()); + assertEquals(PGPLiteralData.CONSOLE, decResult.getFilename()); JUtils.assertDateEquals(PGPLiteralData.NOW, decResult.getModificationDate()); - assertEquals(PGPLiteralData.BINARY, decResult.getFileEncoding().getCode()); + assertEquals(PGPLiteralData.BINARY, decResult.getLiteralDataEncoding().getCode()); assertTrue(decResult.isForYourEyesOnly()); } } From 6926cedf615dc0f079f7b96394328f64a6f92fe9 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 22 Nov 2022 15:41:58 +0100 Subject: [PATCH 147/763] Fix compilation errors and simplify LayerIterator by introducing Packet interface --- ...oundTripInlineSignInlineVerifyCmdTest.java | 2 +- .../MessageMetadata.java | 53 +++++++------------ .../OpenPgpMetadata.java | 2 +- .../pgpainless/sop/DetachedVerifyImpl.java | 4 +- 4 files changed, 24 insertions(+), 37 deletions(-) diff --git a/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/RoundTripInlineSignInlineVerifyCmdTest.java b/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/RoundTripInlineSignInlineVerifyCmdTest.java index 1e5310f5..d36ee58f 100644 --- a/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/RoundTripInlineSignInlineVerifyCmdTest.java +++ b/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/RoundTripInlineSignInlineVerifyCmdTest.java @@ -287,7 +287,7 @@ public class RoundTripInlineSignInlineVerifyCmdTest extends CLITest { File cert = writeFile("cert.asc", CERT_1); pipeStringToStdin(msgOut.toString()); ByteArrayOutputStream verificationsOut = pipeStdoutToStream(); - assertSuccess(executeCommand("verify", + assertSuccess(executeCommand("verify", "--stacktrace", sigFile.getAbsolutePath(), cert.getAbsolutePath())); diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/MessageMetadata.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/MessageMetadata.java index 5318fcb1..da6902c5 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/MessageMetadata.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/MessageMetadata.java @@ -9,7 +9,6 @@ import java.util.Date; import java.util.Iterator; import java.util.List; import java.util.NoSuchElementException; -import java.util.function.Function; import javax.annotation.Nonnull; import javax.annotation.Nullable; @@ -92,7 +91,7 @@ public class MessageMetadata { public @Nonnull Iterator getEncryptionLayers() { return new LayerIterator(message) { @Override - public boolean matches(Nested layer) { + public boolean matches(Packet layer) { return layer instanceof EncryptedData; } @@ -128,7 +127,7 @@ public class MessageMetadata { public @Nonnull Iterator getCompressionLayers() { return new LayerIterator(message) { @Override - boolean matches(Layer layer) { + boolean matches(Packet layer) { return layer instanceof CompressedData; } @@ -242,15 +241,10 @@ public class MessageMetadata { public @Nonnull Iterator> getVerifiedInlineSignaturesByLayer() { return new LayerIterator>(message) { @Override - boolean matches(Nested layer) { + boolean matches(Packet layer) { return layer instanceof Layer; } - @Override - boolean matches(Layer layer) { - return true; - } - @Override List getProperty(Layer last) { List list = new ArrayList<>(); @@ -284,15 +278,10 @@ public class MessageMetadata { public @Nonnull Iterator> getRejectedInlineSignaturesByLayer() { return new LayerIterator>(message) { @Override - boolean matches(Nested layer) { + boolean matches(Packet layer) { return layer instanceof Layer; } - @Override - boolean matches(Layer layer) { - return true; - } - @Override List getProperty(Layer last) { List list = new ArrayList<>(); @@ -334,10 +323,10 @@ public class MessageMetadata { * @return filename * @see RFC4880 §5.9. Literal Data Packet */ - public @Nonnull String getFilename() { + public @Nullable String getFilename() { LiteralData literalData = findLiteralData(); if (literalData == null) { - throw new NoSuchElementException("No Literal Data Packet found."); + return null; } return literalData.getFileName(); } @@ -359,10 +348,10 @@ public class MessageMetadata { * @return modification date * @see RFC4880 §5.9. Literal Data Packet */ - public @Nonnull Date getModificationDate() { + public @Nullable Date getModificationDate() { LiteralData literalData = findLiteralData(); if (literalData == null) { - throw new NoSuchElementException("No Literal Data Packet found."); + return null; } return literalData.getModificationDate(); } @@ -375,10 +364,10 @@ public class MessageMetadata { * @return format * @see RFC4880 §5.9. Literal Data Packet */ - public @Nonnull StreamEncoding getLiteralDataEncoding() { + public @Nullable StreamEncoding getLiteralDataEncoding() { LiteralData literalData = findLiteralData(); if (literalData == null) { - throw new NoSuchElementException("No Literal Data Packet found."); + return null; } return literalData.getFormat(); } @@ -414,7 +403,10 @@ public class MessageMetadata { return firstOrNull(map(getEncryptionLayers(), encryptedData -> encryptedData.decryptionKey)); } - public abstract static class Layer { + public interface Packet { + + } + public abstract static class Layer implements Packet { public static final int MAX_LAYER_DEPTH = 16; protected final int depth; protected final List verifiedDetachedSignatures = new ArrayList<>(); @@ -562,7 +554,7 @@ public class MessageMetadata { } - public interface Nested { + public interface Nested extends Packet { boolean hasNestedChild(); } @@ -760,16 +752,7 @@ public class MessageMetadata { } } - boolean matches(Nested layer) { - return false; - } - - boolean matches(Layer layer) { - if (layer instanceof Nested) { - return matches((Nested) layer); - } - return false; - } + abstract boolean matches(Packet layer); abstract O getProperty(Layer last); } @@ -788,6 +771,10 @@ public class MessageMetadata { }; } + public interface Function { + B apply(A item); + } + private static @Nullable A firstOrNull(Iterator iterator) { if (iterator.hasNext()) { return iterator.next(); diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMetadata.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMetadata.java index e26fb804..be9cf2b1 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMetadata.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMetadata.java @@ -328,7 +328,7 @@ public class OpenPgpMetadata { return this; } - public Builder setFileName(@Nonnull String fileName) { + public Builder setFileName(@Nullable String fileName) { this.fileName = fileName; return this; } diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/DetachedVerifyImpl.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/DetachedVerifyImpl.java index 3a5b3b6a..1ed43941 100644 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/DetachedVerifyImpl.java +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/DetachedVerifyImpl.java @@ -16,7 +16,7 @@ import org.bouncycastle.util.io.Streams; import org.pgpainless.PGPainless; import org.pgpainless.decryption_verification.ConsumerOptions; import org.pgpainless.decryption_verification.DecryptionStream; -import org.pgpainless.decryption_verification.OpenPgpMetadata; +import org.pgpainless.decryption_verification.MessageMetadata; import org.pgpainless.decryption_verification.SignatureVerification; import org.pgpainless.exception.MalformedOpenPgpMessageException; import sop.Verification; @@ -69,7 +69,7 @@ public class DetachedVerifyImpl implements DetachedVerify { Streams.drain(decryptionStream); decryptionStream.close(); - OpenPgpMetadata metadata = decryptionStream.getResult(); + MessageMetadata metadata = decryptionStream.getMetadata(); List verificationList = new ArrayList<>(); for (SignatureVerification signatureVerification : metadata.getVerifiedDetachedSignatures()) { From c031ea92856360fea41a6717bd99ef6ea52289ef Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 22 Nov 2022 15:51:31 +0100 Subject: [PATCH 148/763] Remove empty newlines --- .../test/java/org/pgpainless/example/ManagePolicy.java | 6 ------ .../src/test/java/org/pgpainless/example/ModifyKeys.java | 8 -------- .../src/test/java/org/pgpainless/example/ReadKeys.java | 2 -- .../java/org/pgpainless/example/UnlockSecretKeys.java | 2 -- 4 files changed, 18 deletions(-) diff --git a/pgpainless-core/src/test/java/org/pgpainless/example/ManagePolicy.java b/pgpainless-core/src/test/java/org/pgpainless/example/ManagePolicy.java index 711de225..858c99a9 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/example/ManagePolicy.java +++ b/pgpainless-core/src/test/java/org/pgpainless/example/ManagePolicy.java @@ -88,7 +88,6 @@ public class ManagePolicy { // Per default, non-revocation signatures using SHA-1 are rejected assertFalse(sigHashAlgoPolicy.isAcceptable(HashAlgorithm.SHA1)); - // Create a new custom policy which contains SHA-1 Policy.HashAlgorithmPolicy customPolicy = new Policy.HashAlgorithmPolicy( // The default hash algorithm will be used when hash algorithm negotiation fails when creating a sig @@ -98,7 +97,6 @@ public class ManagePolicy { // Set the hash algo policy as policy for non-revocation signatures policy.setSignatureHashAlgorithmPolicy(customPolicy); - sigHashAlgoPolicy = policy.getSignatureHashAlgorithmPolicy(); assertTrue(sigHashAlgoPolicy.isAcceptable(HashAlgorithm.SHA512)); // SHA-1 is now acceptable as well @@ -122,7 +120,6 @@ public class ManagePolicy { assertFalse(pkAlgorithmPolicy.isAcceptable(PublicKeyAlgorithm.RSA_GENERAL, 1024)); assertTrue(pkAlgorithmPolicy.isAcceptable(PublicKeyAlgorithm.ECDSA, 256)); - Policy.PublicKeyAlgorithmPolicy customPolicy = new Policy.PublicKeyAlgorithmPolicy( new HashMap(){{ // Put minimum bit strengths for acceptable algorithms. @@ -133,7 +130,6 @@ public class ManagePolicy { ); policy.setPublicKeyAlgorithmPolicy(customPolicy); - pkAlgorithmPolicy = policy.getPublicKeyAlgorithmPolicy(); assertTrue(pkAlgorithmPolicy.isAcceptable(PublicKeyAlgorithm.RSA_GENERAL, 4096)); // RSA 2048 is no longer acceptable @@ -156,10 +152,8 @@ public class ManagePolicy { NotationRegistry notationRegistry = policy.getNotationRegistry(); assertFalse(notationRegistry.isKnownNotation("unknown@pgpainless.org")); - notationRegistry.addKnownNotation("unknown@pgpainless.org"); - assertTrue(notationRegistry.isKnownNotation("unknown@pgpainless.org")); } } diff --git a/pgpainless-core/src/test/java/org/pgpainless/example/ModifyKeys.java b/pgpainless-core/src/test/java/org/pgpainless/example/ModifyKeys.java index a0785993..768064e7 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/example/ModifyKeys.java +++ b/pgpainless-core/src/test/java/org/pgpainless/example/ModifyKeys.java @@ -67,7 +67,6 @@ public class ModifyKeys { // the certificate consists of only the public keys PGPPublicKeyRing certificate = PGPainless.extractCertificate(secretKey); - KeyRingInfo info = PGPainless.inspectKeyRing(certificate); assertFalse(info.isSecretKey()); } @@ -79,11 +78,9 @@ public class ModifyKeys { public void toAsciiArmoredString() throws IOException { PGPPublicKeyRing certificate = PGPainless.extractCertificate(secretKey); - String asciiArmoredSecretKey = PGPainless.asciiArmor(secretKey); String asciiArmoredCertificate = PGPainless.asciiArmor(certificate); - assertTrue(asciiArmoredSecretKey.startsWith("-----BEGIN PGP PRIVATE KEY BLOCK-----")); assertTrue(asciiArmoredCertificate.startsWith("-----BEGIN PGP PUBLIC KEY BLOCK-----")); } @@ -99,7 +96,6 @@ public class ModifyKeys { .toNewPassphrase(Passphrase.fromPassword("n3wP4ssW0rD")) .done(); - // Old passphrase no longer works assertThrows(WrongPassphraseException.class, () -> UnlockSecretKey.unlockSecretKey(secretKey.getSecretKey(), Passphrase.fromPassword(originalPassphrase))); @@ -120,7 +116,6 @@ public class ModifyKeys { .toNewPassphrase(Passphrase.fromPassword("cryptP4ssphr4s3")) .done(); - // encryption key can now only be unlocked using the new passphrase assertThrows(WrongPassphraseException.class, () -> UnlockSecretKey.unlockSecretKey( @@ -143,7 +138,6 @@ public class ModifyKeys { .addUserId("additional@user.id", protector) .done(); - KeyRingInfo info = PGPainless.inspectKeyRing(secretKey); assertTrue(info.isUserIdValid("additional@user.id")); assertFalse(info.isUserIdValid("another@user.id")); @@ -176,7 +170,6 @@ public class ModifyKeys { protector) .done(); - KeyRingInfo info = PGPainless.inspectKeyRing(secretKey); assertEquals(4, info.getSecretKeys().size()); assertEquals(4, info.getPublicKeys().size()); @@ -199,7 +192,6 @@ public class ModifyKeys { .setExpirationDate(expirationDate, protector) .done(); - KeyRingInfo info = PGPainless.inspectKeyRing(secretKey); assertEquals(DateUtil.formatUTCDate(expirationDate), DateUtil.formatUTCDate(info.getPrimaryKeyExpirationDate())); diff --git a/pgpainless-core/src/test/java/org/pgpainless/example/ReadKeys.java b/pgpainless-core/src/test/java/org/pgpainless/example/ReadKeys.java index 60256c06..54e00c26 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/example/ReadKeys.java +++ b/pgpainless-core/src/test/java/org/pgpainless/example/ReadKeys.java @@ -44,7 +44,6 @@ public class ReadKeys { PGPPublicKeyRing publicKey = PGPainless.readKeyRing() .publicKeyRing(certificate); - KeyRingInfo keyInfo = new KeyRingInfo(publicKey); OpenPgpFingerprint fingerprint = new OpenPgpV4Fingerprint("EB85 BB5F A33A 75E1 5E94 4E63 F231 550C 4F47 E38E"); assertEquals(fingerprint, keyInfo.getFingerprint()); @@ -77,7 +76,6 @@ public class ReadKeys { PGPSecretKeyRing secretKey = PGPainless.readKeyRing() .secretKeyRing(key); - KeyRingInfo keyInfo = new KeyRingInfo(secretKey); OpenPgpFingerprint fingerprint = new OpenPgpV4Fingerprint("EB85 BB5F A33A 75E1 5E94 4E63 F231 550C 4F47 E38E"); assertEquals(fingerprint, keyInfo.getFingerprint()); diff --git a/pgpainless-core/src/test/java/org/pgpainless/example/UnlockSecretKeys.java b/pgpainless-core/src/test/java/org/pgpainless/example/UnlockSecretKeys.java index c6f227f3..92387978 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/example/UnlockSecretKeys.java +++ b/pgpainless-core/src/test/java/org/pgpainless/example/UnlockSecretKeys.java @@ -40,7 +40,6 @@ public class UnlockSecretKeys { // This protector will only unlock unprotected keys SecretKeyRingProtector protector = SecretKeyRingProtector.unprotectedKeys(); - assertProtectorUnlocksAllSecretKeys(unprotectedKey, protector); } @@ -105,7 +104,6 @@ public class UnlockSecretKeys { protector.addPassphrase(new OpenPgpV4Fingerprint("DD8E1195E4B1720E7FB10EF7F60402708E75D941"), Passphrase.fromPassword("s3c0ndsubk3y")); - assertProtectorUnlocksAllSecretKeys(secretKey, protector); } From f005885318321b397b83371de3e6efc874c09ccd Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 22 Nov 2022 15:52:04 +0100 Subject: [PATCH 149/763] Add MessageMetadata.isVerifiedSigned() and .getVerifiedSignatures() --- .../decryption_verification/MessageMetadata.java | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/MessageMetadata.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/MessageMetadata.java index da6902c5..7743d32e 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/MessageMetadata.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/MessageMetadata.java @@ -186,6 +186,12 @@ public class MessageMetadata { return isVerifiedInlineSignedBy(keys) || isVerifiedDetachedSignedBy(keys); } + public List getVerifiedSignatures() { + List allVerifiedSignatures = getVerifiedInlineSignatures(); + allVerifiedSignatures.addAll(getVerifiedDetachedSignatures()); + return allVerifiedSignatures; + } + public boolean isVerifiedDetachedSignedBy(@Nonnull PGPKeyRing keys) { return containsSignatureBy(getVerifiedDetachedSignatures(), keys); } @@ -403,6 +409,10 @@ public class MessageMetadata { return firstOrNull(map(getEncryptionLayers(), encryptedData -> encryptedData.decryptionKey)); } + public boolean isVerifiedSigned() { + return !getVerifiedSignatures().isEmpty(); + } + public interface Packet { } From 27fd15a01262949b77f9854aa8f53d907980335d Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 22 Nov 2022 15:52:15 +0100 Subject: [PATCH 150/763] Update examples with new MessageMetadata class --- .../pgpainless/example/DecryptOrVerify.java | 21 +++++----- .../java/org/pgpainless/example/Encrypt.java | 42 +++++++++---------- 2 files changed, 30 insertions(+), 33 deletions(-) diff --git a/pgpainless-core/src/test/java/org/pgpainless/example/DecryptOrVerify.java b/pgpainless-core/src/test/java/org/pgpainless/example/DecryptOrVerify.java index 074fc206..2e6c982f 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/example/DecryptOrVerify.java +++ b/pgpainless-core/src/test/java/org/pgpainless/example/DecryptOrVerify.java @@ -24,7 +24,7 @@ import org.pgpainless.PGPainless; import org.pgpainless.algorithm.DocumentSignatureType; import org.pgpainless.decryption_verification.ConsumerOptions; import org.pgpainless.decryption_verification.DecryptionStream; -import org.pgpainless.decryption_verification.OpenPgpMetadata; +import org.pgpainless.decryption_verification.MessageMetadata; import org.pgpainless.encryption_signing.EncryptionStream; import org.pgpainless.encryption_signing.ProducerOptions; import org.pgpainless.encryption_signing.SigningOptions; @@ -168,9 +168,9 @@ public class DecryptOrVerify { decryptionStream.close(); // remember to close the stream! // The metadata object contains information about the message - OpenPgpMetadata metadata = decryptionStream.getResult(); + MessageMetadata metadata = decryptionStream.getMetadata(); assertTrue(metadata.isEncrypted()); // message was encrypted - assertFalse(metadata.isVerified()); // We did not do any signature verification + assertFalse(metadata.isVerifiedSigned()); // We did not do any signature verification // The output stream now contains the decrypted message assertEquals(PLAINTEXT, plaintextOut.toString()); @@ -200,11 +200,10 @@ public class DecryptOrVerify { decryptionStream.close(); // remember to close the stream to finish signature verification // metadata with information on the message, like signatures - OpenPgpMetadata metadata = decryptionStream.getResult(); + MessageMetadata metadata = decryptionStream.getMetadata(); assertTrue(metadata.isEncrypted()); // messages was in fact encrypted - assertTrue(metadata.isSigned()); // message contained some signatures - assertTrue(metadata.isVerified()); // the signatures were actually correct - assertTrue(metadata.containsVerifiedSignatureFrom(certificate)); // the signatures could be verified using the certificate + assertTrue(metadata.isVerifiedSigned()); // the signatures were actually correct + assertTrue(metadata.isVerifiedSignedBy(certificate)); // the signatures could be verified using the certificate assertEquals(PLAINTEXT, plaintextOut.toString()); } @@ -232,8 +231,8 @@ public class DecryptOrVerify { verificationStream.close(); // remember to close the stream to finish sig verification // Get the metadata object for information about the message - OpenPgpMetadata metadata = verificationStream.getResult(); - assertTrue(metadata.isVerified()); // signatures were verified successfully + MessageMetadata metadata = verificationStream.getMetadata(); + assertTrue(metadata.isVerifiedSigned()); // signatures were verified successfully // The output stream we piped to now contains the message assertEquals(PLAINTEXT, out.toString()); } @@ -286,8 +285,8 @@ public class DecryptOrVerify { verificationStream.close(); // as always, remember to close the stream // Metadata will confirm that the message was in fact signed - OpenPgpMetadata metadata = verificationStream.getResult(); - assertTrue(metadata.isVerified()); + MessageMetadata metadata = verificationStream.getMetadata(); + assertTrue(metadata.isVerifiedSigned()); // compare the plaintext to what we originally signed assertArrayEquals(msg.getBytes(StandardCharsets.UTF_8), plain.toByteArray()); } diff --git a/pgpainless-core/src/test/java/org/pgpainless/example/Encrypt.java b/pgpainless-core/src/test/java/org/pgpainless/example/Encrypt.java index ed57b90b..ba832516 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/example/Encrypt.java +++ b/pgpainless-core/src/test/java/org/pgpainless/example/Encrypt.java @@ -21,7 +21,7 @@ import org.pgpainless.PGPainless; import org.pgpainless.algorithm.DocumentSignatureType; import org.pgpainless.decryption_verification.ConsumerOptions; import org.pgpainless.decryption_verification.DecryptionStream; -import org.pgpainless.decryption_verification.OpenPgpMetadata; +import org.pgpainless.decryption_verification.MessageMetadata; import org.pgpainless.encryption_signing.EncryptionOptions; import org.pgpainless.encryption_signing.EncryptionStream; import org.pgpainless.encryption_signing.ProducerOptions; @@ -148,12 +148,12 @@ public class Encrypt { EncryptionStream encryptor = PGPainless.encryptAndOrSign() .onOutputStream(ciphertext) .withOptions(ProducerOptions.signAndEncrypt( - // we want to encrypt communication (affects key selection based on key flags) - EncryptionOptions.encryptCommunications() - .addRecipient(certificateBob) - .addRecipient(certificateAlice), - new SigningOptions() - .addInlineSignature(protectorAlice, keyAlice, DocumentSignatureType.CANONICAL_TEXT_DOCUMENT) + // we want to encrypt communication (affects key selection based on key flags) + EncryptionOptions.encryptCommunications() + .addRecipient(certificateBob) + .addRecipient(certificateAlice), + new SigningOptions() + .addInlineSignature(protectorAlice, keyAlice, DocumentSignatureType.CANONICAL_TEXT_DOCUMENT) ).setAsciiArmor(true) ); @@ -176,9 +176,9 @@ public class Encrypt { decryptor.close(); // Check the metadata to see how the message was encrypted/signed - OpenPgpMetadata metadata = decryptor.getResult(); + MessageMetadata metadata = decryptor.getMetadata(); assertTrue(metadata.isEncrypted()); - assertTrue(metadata.containsVerifiedSignatureFrom(certificateAlice)); + assertTrue(metadata.isVerifiedSignedBy(certificateAlice)); assertEquals(message, plaintext.toString()); } @@ -236,10 +236,10 @@ public class Encrypt { // plaintext message to encrypt String message = "Hello, World!\n"; String[] comments = { - "This comment was added using options.", - "And it has three lines.", - " ", - "Empty lines are skipped." + "This comment was added using options.", + "And it has three lines.", + " ", + "Empty lines are skipped." }; String comment = comments[0] + "\n" + comments[1] + "\n" + comments[2] + "\n" + comments[3]; ByteArrayOutputStream ciphertext = new ByteArrayOutputStream(); @@ -247,12 +247,12 @@ public class Encrypt { EncryptionStream encryptor = PGPainless.encryptAndOrSign() .onOutputStream(ciphertext) .withOptions(ProducerOptions.encrypt( - // we want to encrypt communication (affects key selection based on key flags) - EncryptionOptions.encryptCommunications() - .addRecipient(certificateBob) - .addRecipient(certificateAlice) - ).setAsciiArmor(true) - .setComment(comment) + // we want to encrypt communication (affects key selection based on key flags) + EncryptionOptions.encryptCommunications() + .addRecipient(certificateBob) + .addRecipient(certificateAlice) + ).setAsciiArmor(true) + .setComment(comment) ); // Pipe data trough and CLOSE the stream (important) @@ -281,10 +281,8 @@ public class Encrypt { decryptor.close(); // Check the metadata to see how the message was encrypted/signed - OpenPgpMetadata metadata = decryptor.getResult(); + MessageMetadata metadata = decryptor.getMetadata(); assertTrue(metadata.isEncrypted()); assertEquals(message, plaintext.toString()); } - - } From 2c7801b7590e4080bb5964997b239f44c782304c Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 22 Nov 2022 16:09:37 +0100 Subject: [PATCH 151/763] Add MatchMakingSecretKeyRingProtectorTest --- ...MatchMakingSecretKeyRingProtectorTest.java | 108 ++++++++++++++++++ 1 file changed, 108 insertions(+) create mode 100644 pgpainless-sop/src/test/java/org/pgpainless/sop/MatchMakingSecretKeyRingProtectorTest.java diff --git a/pgpainless-sop/src/test/java/org/pgpainless/sop/MatchMakingSecretKeyRingProtectorTest.java b/pgpainless-sop/src/test/java/org/pgpainless/sop/MatchMakingSecretKeyRingProtectorTest.java new file mode 100644 index 00000000..9dc2b6c6 --- /dev/null +++ b/pgpainless-sop/src/test/java/org/pgpainless/sop/MatchMakingSecretKeyRingProtectorTest.java @@ -0,0 +1,108 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.sop; + +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.IOException; + +import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.PGPSecretKeyRing; +import org.junit.jupiter.api.Test; +import org.pgpainless.PGPainless; +import org.pgpainless.util.Passphrase; + +public class MatchMakingSecretKeyRingProtectorTest { + + private static final String PROTECTED_KEY = "-----BEGIN PGP PRIVATE KEY BLOCK-----\n" + + "Version: PGPainless\n" + + "Comment: 0221 626A 3B5A 4705 7A41 7EAB 2B9F C90E 44FA 1947\n" + + "Comment: Alice\n" + + "\n" + + "lIYEY3zkcRYJKwYBBAHaRw8BAQdAzww+ctlV7imTD/LSQlVn3onybSvQa54CIUaN\n" + + "xN9FDFH+CQMCqDw0ZfofkfxgK7+uSfi7btqa6+o+zGkKfKQCvYCuU5gorD7vyOFL\n" + + "2ezeQOjb17HHaKbJqLrx+p+LS2uU2f3cwa73PFHwNcBoDLRTrUXjzrQFQWxpY2WI\n" + + "jwQTFgoAQQUCY3zkcQkQK5/JDkT6GUcWIQQCIWJqO1pHBXpBfqsrn8kORPoZRwKe\n" + + "AQKbAQUWAgMBAAQLCQgHBRUKCQgLApkBAABTCQD9HCDmb8LlO+n/5jJv7n6gAHCA\n" + + "UUNAe7xU4WcYSxLpTPIBAOxBmbZiDai0QwDOqNihpwrInu82fRi8OEpSjE/9OrEC\n" + + "nIsEY3zkcRIKKwYBBAGXVQEFAQEHQBsnJtVYXMaGB4BDcUEKB1v/lsXJ1z+favfn\n" + + "e73/crYEAwEIB/4JAwKoPDRl+h+R/GBdZY7QJt8TPaXckyOR1eZvUejD+Vw/slB1\n" + + "3KUwGI/3MG2iJYp924wP67DewZI89eYHu24wN75XxVKAGnUX5n7Dr2JIB79liHUE\n" + + "GBYKAB0FAmN85HECngECmwwFFgIDAQAECwkIBwUVCgkICwAKCRArn8kORPoZR5bF\n" + + "APsHLmhDRDV2Ra0BsfQRNI2yMXxVRFD/ZzryWFT/BPNGUAEA9cnhItp9ucqBbeWE\n" + + "PDzf1vdx5BCNYhRpOqGjGtFgMQGchgRjfORxFgkrBgEEAdpHDwEBB0AAvsniUT76\n" + + "OeLyq9e1bgdsDLiGrtroSt6wR/B94Dm5uP4JAwKoPDRl+h+R/GC8mQnynSzBJXdy\n" + + "DFDnxOieEOh7390vs3P4NwULTqV12sAQ6i5MbsIHnFMtYCCA9aOPlpofQ0Sm3m6q\n" + + "T/uyx9RE1LRiceW5iNUEGBYKAH0FAmN85HECngECmwIFFgIDAQAECwkIBwUVCgkI\n" + + "C18gBBkWCgAGBQJjfORxAAoJEGiOcMMZDPmyw7QBAJuNTLiNWgieuGOVCAmkaN6g\n" + + "L6JlYYwqFS88zzDLJJq5AQDwKt+jvKco6Mya3b1NEXogBLhWHTle9deL07NrCwp4\n" + + "AwAKCRArn8kORPoZR7r0AP0TDUKaooNfW2MWqLWHbbIhdWFIQEYIGnGSFj28y6t1\n" + + "zQD/UFtpzBP5ZlTUtZCdjNqo9SEPktbiOxTS8m4SW7xeNwE=\n" + + "=91/N\n" + + "-----END PGP PRIVATE KEY BLOCK-----\n"; + private static final String PASSWORD = "sw0rdf1sh"; + private static final String UNPROTECTED_KEY = "-----BEGIN PGP PRIVATE KEY BLOCK-----\n" + + "Version: PGPainless\n" + + "Comment: 9A0A F461 3E00 E1D4 4C29 601E AAF1 12F4 64BB 1E8E\n" + + "Comment: Bob\n" + + "\n" + + "lFgEY3zlDBYJKwYBBAHaRw8BAQdAvHofWedfSBvyW2+gCADX9CptwFzqVea4A2tL\n" + + "zr3wnwsAAP9ICAoMGkgdNLy3LiVP0q4+OljXcQTIAJbJ2wCpIF9Y7g05tANCb2KI\n" + + "jwQTFgoAQQUCY3zlDAkQqvES9GS7Ho4WIQSaCvRhPgDh1EwpYB6q8RL0ZLsejgKe\n" + + "AQKbAQUWAgMBAAQLCQgHBRUKCQgLApkBAAAZ8QD/fEW105B77KBt/OmA0QLTq3GG\n" + + "5PI6kITM8+2cd60VOzEA/ivzkhmtdvHzmOARBl81Y3LfeRWWm45z/dYDnffk/DcI\n" + + "nF0EY3zlDBIKKwYBBAGXVQEFAQEHQC1sYpvzEsjCoTOKEllFkWA3U51FXsHbbALq\n" + + "QfprOrYKAwEIBwAA/3H4zdk83/0A55hJxBIgh3v/+EV1RKPDCjMHjI5ULc7AEa6I\n" + + "dQQYFgoAHQUCY3zlDAKeAQKbDAUWAgMBAAQLCQgHBRUKCQgLAAoJEKrxEvRkux6O\n" + + "pEsA/imXUEpj6mKkT4ZBioT7Gn2mUR4iMGS/pt7QBscDX2/PAP9FFRzsaDII1K+i\n" + + "zW5sHEif9EjgX6ThIpg8z4/5/7yQBZxYBGN85QwWCSsGAQQB2kcPAQEHQDTAYxP0\n" + + "rH0tjpOKOxdoHKq87n4tYXd1t/A9Nzjbl36AAAD+PMBIpNmN+k3THARd9UGQtLo4\n" + + "nieLnqbuPVtMps0kQjgQxojVBBgWCgB9BQJjfOUMAp4BApsCBRYCAwEABAsJCAcF\n" + + "FQoJCAtfIAQZFgoABgUCY3zlDAAKCRB0YfQ676jh4zoMAP98SwGcoy8Vzk8QnQ0X\n" + + "gziC+4HtmTLuiDVAvrMLpPz5cwD8C40DDHEjrOJs9bgyOeTELXtjq40Wrt2Fld0G\n" + + "3JJpFAwACgkQqvES9GS7Ho7EXwD7BwICVWrg458XKpy2EXGSI3mGA47EbyyFc9X3\n" + + "lBzjnCgA/jUBlZE2LhpAyMTbjDC9eAD1iXeTALdRKBeqnZrQTL0N\n" + + "=OypZ\n" + + "-----END PGP PRIVATE KEY BLOCK-----\n"; + + @Test + public void addSamePasswordTwice() throws IOException { + PGPSecretKeyRing key = PGPainless.readKeyRing().secretKeyRing(PROTECTED_KEY); + MatchMakingSecretKeyRingProtector protector = new MatchMakingSecretKeyRingProtector(); + protector.addPassphrase(Passphrase.fromPassword(PASSWORD)); + protector.addPassphrase(Passphrase.fromPassword(PASSWORD)); + protector.addSecretKey(key); + + assertTrue(protector.hasPassphraseFor(key.getPublicKey().getKeyID())); + } + + @Test + public void addKeyTwiceAndEmptyPasswordTest() throws IOException { + PGPSecretKeyRing unprotectedKey = PGPainless.readKeyRing().secretKeyRing(UNPROTECTED_KEY); + MatchMakingSecretKeyRingProtector protector = new MatchMakingSecretKeyRingProtector(); + protector.addSecretKey(unprotectedKey); + protector.addPassphrase(Passphrase.emptyPassphrase()); + protector.addSecretKey(unprotectedKey); + assertTrue(protector.hasPassphraseFor(unprotectedKey.getPublicKey().getKeyID())); + } + + @Test + public void getEncryptorTest() throws IOException, PGPException { + PGPSecretKeyRing unprotectedKey = PGPainless.readKeyRing().secretKeyRing(UNPROTECTED_KEY); + MatchMakingSecretKeyRingProtector protector = new MatchMakingSecretKeyRingProtector(); + protector.addSecretKey(unprotectedKey); + assertTrue(protector.hasPassphraseFor(unprotectedKey.getPublicKey().getKeyID())); + assertNull(protector.getEncryptor(unprotectedKey.getPublicKey().getKeyID())); + assertNull(protector.getDecryptor(unprotectedKey.getPublicKey().getKeyID())); + + PGPSecretKeyRing protectedKey = PGPainless.readKeyRing().secretKeyRing(PROTECTED_KEY); + protector.addSecretKey(protectedKey); + protector.addPassphrase(Passphrase.fromPassword(PASSWORD)); + assertNotNull(protector.getEncryptor(protectedKey.getPublicKey().getKeyID())); + assertNotNull(protector.getDecryptor(protectedKey.getPublicKey().getKeyID())); + } +} From b36b5413e2a93bf0d7a7caba0deffa7bc34a8746 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 22 Nov 2022 16:22:01 +0100 Subject: [PATCH 152/763] Fix isEncryptedFor() --- .../MessageMetadata.java | 3 ++ .../OpenPgpMessageInputStream.java | 46 ++++++++++++------- .../pgpainless/example/DecryptOrVerify.java | 3 ++ 3 files changed, 35 insertions(+), 17 deletions(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/MessageMetadata.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/MessageMetadata.java index 7743d32e..8c18eadb 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/MessageMetadata.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/MessageMetadata.java @@ -698,6 +698,9 @@ public class MessageMetadata { * @return recipients */ public @Nonnull List getRecipients() { + if (recipients == null) { + return new ArrayList<>(); + } return new ArrayList<>(recipients); } diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java index a820dca5..263871de 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java @@ -428,7 +428,7 @@ public class OpenPgpMessageInputStream extends DecryptionStream { } // attempt decryption - if (decryptPKESKAndStream(subkeyIdentifier, decryptorFactory, pkesk)) { + if (decryptPKESKAndStream(esks, subkeyIdentifier, decryptorFactory, pkesk)) { return true; } } @@ -473,7 +473,7 @@ public class OpenPgpMessageInputStream extends DecryptionStream { PBEDataDecryptorFactory decryptorFactory = ImplementationFactory.getInstance() .getPBEDataDecryptorFactory(passphrase); - if (decryptSKESKAndStream(skesk, decryptorFactory)) { + if (decryptSKESKAndStream(esks, skesk, decryptorFactory)) { return true; } } @@ -506,7 +506,7 @@ public class OpenPgpMessageInputStream extends DecryptionStream { } PGPPrivateKey privateKey = UnlockSecretKey.unlockSecretKey(secretKey, protector); - if (decryptWithPrivateKey(privateKey, decryptionKeyId, pkesk)) { + if (decryptWithPrivateKey(esks, privateKey, decryptionKeyId, pkesk)) { return true; } } @@ -529,7 +529,7 @@ public class OpenPgpMessageInputStream extends DecryptionStream { } PGPPrivateKey privateKey = UnlockSecretKey.unlockSecretKey(secretKey, protector); - if (decryptWithPrivateKey(privateKey, decryptionKeyId, pkesk)) { + if (decryptWithPrivateKey(esks, privateKey, decryptionKeyId, pkesk)) { return true; } } @@ -561,7 +561,7 @@ public class OpenPgpMessageInputStream extends DecryptionStream { LOGGER.debug("Attempt decryption with key " + decryptionKeyId + " while interactively requesting its passphrase"); SecretKeyRingProtector protector = options.getSecretKeyProtector(decryptionKey); PGPPrivateKey privateKey = UnlockSecretKey.unlockSecretKey(secretKey, protector); - if (decryptWithPrivateKey(privateKey, decryptionKeyId, pkesk)) { + if (decryptWithPrivateKey(esks, privateKey, decryptionKeyId, pkesk)) { return true; } } @@ -576,13 +576,14 @@ public class OpenPgpMessageInputStream extends DecryptionStream { return false; } - private boolean decryptWithPrivateKey(PGPPrivateKey privateKey, + private boolean decryptWithPrivateKey(SortedESKs esks, + PGPPrivateKey privateKey, SubkeyIdentifier decryptionKeyId, PGPPublicKeyEncryptedData pkesk) throws PGPException, IOException { PublicKeyDataDecryptorFactory decryptorFactory = ImplementationFactory.getInstance() .getPublicKeyDataDecryptorFactory(privateKey); - return decryptPKESKAndStream(decryptionKeyId, decryptorFactory, pkesk); + return decryptPKESKAndStream(esks, decryptionKeyId, decryptorFactory, pkesk); } private static boolean hasUnsupportedS2KSpecifier(PGPSecretKey secretKey, SubkeyIdentifier decryptionKeyId) { @@ -597,17 +598,23 @@ public class OpenPgpMessageInputStream extends DecryptionStream { return false; } - private boolean decryptSKESKAndStream(PGPPBEEncryptedData skesk, PBEDataDecryptorFactory decryptorFactory) + private boolean decryptSKESKAndStream(SortedESKs esks, + PGPPBEEncryptedData symEsk, + PBEDataDecryptorFactory decryptorFactory) throws IOException, UnacceptableAlgorithmException { try { - InputStream decrypted = skesk.getDataStream(decryptorFactory); - SessionKey sessionKey = new SessionKey(skesk.getSessionKey(decryptorFactory)); + InputStream decrypted = symEsk.getDataStream(decryptorFactory); + SessionKey sessionKey = new SessionKey(symEsk.getSessionKey(decryptorFactory)); throwIfUnacceptable(sessionKey.getAlgorithm()); MessageMetadata.EncryptedData encryptedData = new MessageMetadata.EncryptedData( sessionKey.getAlgorithm(), metadata.depth + 1); encryptedData.sessionKey = sessionKey; + encryptedData.recipients = new ArrayList<>(); + for (PGPPublicKeyEncryptedData pkesk : esks.pkesks) { + encryptedData.recipients.add(pkesk.getKeyID()); + } LOGGER.debug("Successfully decrypted data with passphrase"); - IntegrityProtectedInputStream integrityProtected = new IntegrityProtectedInputStream(decrypted, skesk, options); + IntegrityProtectedInputStream integrityProtected = new IntegrityProtectedInputStream(decrypted, symEsk, options); nestedInputStream = new OpenPgpMessageInputStream(integrityProtected, options, encryptedData, policy); return true; } catch (UnacceptableAlgorithmException e) { @@ -618,23 +625,28 @@ public class OpenPgpMessageInputStream extends DecryptionStream { return false; } - private boolean decryptPKESKAndStream(SubkeyIdentifier decryptionKeyId, + private boolean decryptPKESKAndStream(SortedESKs esks, + SubkeyIdentifier decryptionKeyId, PublicKeyDataDecryptorFactory decryptorFactory, - PGPPublicKeyEncryptedData pkesk) + PGPPublicKeyEncryptedData asymEsk) throws IOException, UnacceptableAlgorithmException { try { - InputStream decrypted = pkesk.getDataStream(decryptorFactory); - SessionKey sessionKey = new SessionKey(pkesk.getSessionKey(decryptorFactory)); + InputStream decrypted = asymEsk.getDataStream(decryptorFactory); + SessionKey sessionKey = new SessionKey(asymEsk.getSessionKey(decryptorFactory)); throwIfUnacceptable(sessionKey.getAlgorithm()); MessageMetadata.EncryptedData encryptedData = new MessageMetadata.EncryptedData( - SymmetricKeyAlgorithm.requireFromId(pkesk.getSymmetricAlgorithm(decryptorFactory)), + SymmetricKeyAlgorithm.requireFromId(asymEsk.getSymmetricAlgorithm(decryptorFactory)), metadata.depth + 1); encryptedData.decryptionKey = decryptionKeyId; encryptedData.sessionKey = sessionKey; + encryptedData.recipients = new ArrayList<>(); + for (PGPPublicKeyEncryptedData pkesk : esks.pkesks) { + encryptedData.recipients.add(pkesk.getKeyID()); + } LOGGER.debug("Successfully decrypted data with key " + decryptionKeyId); - IntegrityProtectedInputStream integrityProtected = new IntegrityProtectedInputStream(decrypted, pkesk, options); + IntegrityProtectedInputStream integrityProtected = new IntegrityProtectedInputStream(decrypted, asymEsk, options); nestedInputStream = new OpenPgpMessageInputStream(integrityProtected, options, encryptedData, policy); return true; } catch (UnacceptableAlgorithmException e) { diff --git a/pgpainless-core/src/test/java/org/pgpainless/example/DecryptOrVerify.java b/pgpainless-core/src/test/java/org/pgpainless/example/DecryptOrVerify.java index 2e6c982f..c35b3572 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/example/DecryptOrVerify.java +++ b/pgpainless-core/src/test/java/org/pgpainless/example/DecryptOrVerify.java @@ -170,6 +170,7 @@ public class DecryptOrVerify { // The metadata object contains information about the message MessageMetadata metadata = decryptionStream.getMetadata(); assertTrue(metadata.isEncrypted()); // message was encrypted + assertTrue(metadata.isEncryptedFor(secretKey)); assertFalse(metadata.isVerifiedSigned()); // We did not do any signature verification // The output stream now contains the decrypted message @@ -202,6 +203,7 @@ public class DecryptOrVerify { // metadata with information on the message, like signatures MessageMetadata metadata = decryptionStream.getMetadata(); assertTrue(metadata.isEncrypted()); // messages was in fact encrypted + assertTrue(metadata.isEncryptedFor(certificate)); assertTrue(metadata.isVerifiedSigned()); // the signatures were actually correct assertTrue(metadata.isVerifiedSignedBy(certificate)); // the signatures could be verified using the certificate @@ -233,6 +235,7 @@ public class DecryptOrVerify { // Get the metadata object for information about the message MessageMetadata metadata = verificationStream.getMetadata(); assertTrue(metadata.isVerifiedSigned()); // signatures were verified successfully + assertTrue(metadata.isVerifiedSignedBy(certificate)); // The output stream we piped to now contains the message assertEquals(PLAINTEXT, out.toString()); } From 25190fc5df41ad3a6c2acb245d6abf63ce7d197a Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 22 Nov 2022 16:27:34 +0100 Subject: [PATCH 153/763] SOP: Use new MessageMetadata class --- .../decryption_verification/MessageMetadata.java | 4 ++++ .../src/main/java/org/pgpainless/sop/DecryptImpl.java | 6 +++--- .../main/java/org/pgpainless/sop/InlineVerifyImpl.java | 8 ++++---- 3 files changed, 11 insertions(+), 7 deletions(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/MessageMetadata.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/MessageMetadata.java index 8c18eadb..648b99ab 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/MessageMetadata.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/MessageMetadata.java @@ -69,6 +69,10 @@ public class MessageMetadata { return resultBuilder.build(); } + public boolean isUsingCleartextSignatureFramework() { + return message.isCleartextSigned(); + } + public boolean isEncrypted() { SymmetricKeyAlgorithm algorithm = getEncryptionAlgorithm(); return algorithm != null && algorithm != SymmetricKeyAlgorithm.NULL; diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/DecryptImpl.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/DecryptImpl.java index ee86f5e8..867024ef 100644 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/DecryptImpl.java +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/DecryptImpl.java @@ -21,7 +21,7 @@ import org.pgpainless.PGPainless; import org.pgpainless.algorithm.SymmetricKeyAlgorithm; import org.pgpainless.decryption_verification.ConsumerOptions; import org.pgpainless.decryption_verification.DecryptionStream; -import org.pgpainless.decryption_verification.OpenPgpMetadata; +import org.pgpainless.decryption_verification.MessageMetadata; import org.pgpainless.decryption_verification.SignatureVerification; import org.pgpainless.exception.MalformedOpenPgpMessageException; import org.pgpainless.exception.MissingDecryptionMethodException; @@ -136,14 +136,14 @@ public class DecryptImpl implements Decrypt { public DecryptionResult writeTo(OutputStream outputStream) throws IOException, SOPGPException.NoSignature { Streams.pipeAll(decryptionStream, outputStream); decryptionStream.close(); - OpenPgpMetadata metadata = decryptionStream.getResult(); + MessageMetadata metadata = decryptionStream.getMetadata(); if (!metadata.isEncrypted()) { throw new SOPGPException.BadData("Data is not encrypted."); } List verificationList = new ArrayList<>(); - for (SignatureVerification signatureVerification : metadata.getVerifiedInbandSignatures()) { + for (SignatureVerification signatureVerification : metadata.getVerifiedInlineSignatures()) { verificationList.add(map(signatureVerification)); } diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/InlineVerifyImpl.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/InlineVerifyImpl.java index 82ed4282..f33f718b 100644 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/InlineVerifyImpl.java +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/InlineVerifyImpl.java @@ -17,7 +17,7 @@ import org.bouncycastle.util.io.Streams; import org.pgpainless.PGPainless; import org.pgpainless.decryption_verification.ConsumerOptions; import org.pgpainless.decryption_verification.DecryptionStream; -import org.pgpainless.decryption_verification.OpenPgpMetadata; +import org.pgpainless.decryption_verification.MessageMetadata; import org.pgpainless.decryption_verification.SignatureVerification; import org.pgpainless.exception.MalformedOpenPgpMessageException; import org.pgpainless.exception.MissingDecryptionMethodException; @@ -63,12 +63,12 @@ public class InlineVerifyImpl implements InlineVerify { Streams.pipeAll(decryptionStream, outputStream); decryptionStream.close(); - OpenPgpMetadata metadata = decryptionStream.getResult(); + MessageMetadata metadata = decryptionStream.getMetadata(); List verificationList = new ArrayList<>(); - List verifications = metadata.isCleartextSigned() ? + List verifications = metadata.isUsingCleartextSignatureFramework() ? metadata.getVerifiedDetachedSignatures() : - metadata.getVerifiedInbandSignatures(); + metadata.getVerifiedInlineSignatures(); for (SignatureVerification signatureVerification : verifications) { verificationList.add(map(signatureVerification)); From b495e602e58b606472730670750f2185c83b57f2 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 22 Nov 2022 16:30:06 +0100 Subject: [PATCH 154/763] More precise error message for malformed message --- .../pgpainless/decryption_verification/syntax_check/PDA.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/PDA.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/PDA.java index 07a5fdcb..adc5bb39 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/PDA.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/PDA.java @@ -55,7 +55,7 @@ public class PDA { inputs.add(input); } catch (MalformedOpenPgpMessageException e) { MalformedOpenPgpMessageException wrapped = new MalformedOpenPgpMessageException( - "Malformed message: After reading stream " + Arrays.toString(inputs.toArray()) + + "Malformed message: After reading packet sequence " + Arrays.toString(inputs.toArray()) + ", token '" + input + "' is not allowed." + "\nNo transition from state '" + state + "' with stack " + Arrays.toString(stack.toArray()) + (stackSymbol != null ? "||'" + stackSymbol + "'." : "."), e); From be7349f0b52976f93e4e287b0fd4653a3fbc135d Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 23 Nov 2022 20:07:03 +0100 Subject: [PATCH 155/763] Clean up CachingBcPublicKeyDataDecryptorFactory --- .../CachingBcPublicKeyDataDecryptorFactory.java | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/pgpainless-core/src/main/java/org/bouncycastle/CachingBcPublicKeyDataDecryptorFactory.java b/pgpainless-core/src/main/java/org/bouncycastle/CachingBcPublicKeyDataDecryptorFactory.java index 3c967224..510b0938 100644 --- a/pgpainless-core/src/main/java/org/bouncycastle/CachingBcPublicKeyDataDecryptorFactory.java +++ b/pgpainless-core/src/main/java/org/bouncycastle/CachingBcPublicKeyDataDecryptorFactory.java @@ -59,16 +59,20 @@ public class CachingBcPublicKeyDataDecryptorFactory } private byte[] lookupSessionKeyData(byte[][] secKeyData) { - byte[] sk = secKeyData[0]; - String key = Base64.toBase64String(sk); + String key = toKey(secKeyData); byte[] sessionKey = cachedSessionKeys.get(key); return copy(sessionKey); } private void cacheSessionKeyData(byte[][] secKeyData, byte[] sessionKey) { + String key = toKey(secKeyData); + cachedSessionKeys.put(key, copy(sessionKey)); + } + + private static String toKey(byte[][] secKeyData) { byte[] sk = secKeyData[0]; String key = Base64.toBase64String(sk); - cachedSessionKeys.put(key, copy(sessionKey)); + return key; } private static byte[] copy(byte[] bytes) { From c72b3a4b8e251a3fa99df0c010a5785b5ce6a7ff Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 23 Nov 2022 20:07:42 +0100 Subject: [PATCH 156/763] Improve CachingBcPublicKeyDataDecryptorFactoryTest --- .../CachingBcPublicKeyDataDecryptorFactoryTest.java | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/pgpainless-core/src/test/java/org/bouncycastle/CachingBcPublicKeyDataDecryptorFactoryTest.java b/pgpainless-core/src/test/java/org/bouncycastle/CachingBcPublicKeyDataDecryptorFactoryTest.java index 6a43772d..e57df5d9 100644 --- a/pgpainless-core/src/test/java/org/bouncycastle/CachingBcPublicKeyDataDecryptorFactoryTest.java +++ b/pgpainless-core/src/test/java/org/bouncycastle/CachingBcPublicKeyDataDecryptorFactoryTest.java @@ -25,6 +25,8 @@ import org.pgpainless.key.info.KeyRingInfo; import org.pgpainless.key.protection.SecretKeyRingProtector; import org.pgpainless.key.protection.UnlockSecretKey; +import static org.junit.jupiter.api.Assertions.assertEquals; + public class CachingBcPublicKeyDataDecryptorFactoryTest { private static final String KEY = "-----BEGIN PGP PRIVATE KEY BLOCK-----\n" + @@ -83,7 +85,9 @@ public class CachingBcPublicKeyDataDecryptorFactoryTest { ByteArrayOutputStream out = new ByteArrayOutputStream(); Streams.pipeAll(decryptionStream, out); decryptionStream.close(); + assertEquals("Hello, World!\n", out.toString()); + ciphertextIn = new ByteArrayInputStream(MSG.getBytes()); decryptionStream = PGPainless.decryptAndOrVerify() .onInputStream(ciphertextIn) .withOptions(ConsumerOptions.get() @@ -91,5 +95,8 @@ public class CachingBcPublicKeyDataDecryptorFactoryTest { out = new ByteArrayOutputStream(); Streams.pipeAll(decryptionStream, out); decryptionStream.close(); + assertEquals("Hello, World!\n", out.toString()); + + cachingFactory.clear(); } } From a495f2275c233c6ed7989697da67823e122d5e26 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 24 Nov 2022 21:34:25 +0100 Subject: [PATCH 157/763] Precise error message for IntegrityProtectedInputStream --- .../decryption_verification/IntegrityProtectedInputStream.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/IntegrityProtectedInputStream.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/IntegrityProtectedInputStream.java index 286160e8..37dcfca4 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/IntegrityProtectedInputStream.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/IntegrityProtectedInputStream.java @@ -53,7 +53,7 @@ public class IntegrityProtectedInputStream extends InputStream { } LOGGER.debug("Integrity Protection check passed"); } catch (PGPException e) { - throw new IOException("Failed to verify integrity protection", e); + throw new IOException("Data appears to not be integrity protected.", e); } } } From 5bdd4f6ad04d05a97011ea55639fd381136d4fb7 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 24 Nov 2022 22:09:22 +0100 Subject: [PATCH 158/763] Test rejection of messages with unacceptable skesk kek algorithm --- .../DecryptAndVerifyMessageTest.java | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/DecryptAndVerifyMessageTest.java b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/DecryptAndVerifyMessageTest.java index f7402238..e939de0a 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/DecryptAndVerifyMessageTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/DecryptAndVerifyMessageTest.java @@ -7,6 +7,7 @@ package org.pgpainless.decryption_verification; import static org.junit.jupiter.api.Assertions.assertArrayEquals; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import java.io.ByteArrayInputStream; @@ -18,14 +19,17 @@ import org.bouncycastle.openpgp.PGPException; import org.bouncycastle.openpgp.PGPSecretKeyRing; import org.bouncycastle.util.io.Streams; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestTemplate; import org.junit.jupiter.api.extension.ExtendWith; import org.pgpainless.PGPainless; import org.pgpainless.algorithm.CompressionAlgorithm; import org.pgpainless.algorithm.SymmetricKeyAlgorithm; +import org.pgpainless.exception.MissingDecryptionMethodException; import org.pgpainless.key.SubkeyIdentifier; import org.pgpainless.key.TestKeys; import org.pgpainless.key.util.KeyRingUtils; +import org.pgpainless.util.Passphrase; import org.pgpainless.util.TestAllImplementations; public class DecryptAndVerifyMessageTest { @@ -118,4 +122,21 @@ public class DecryptAndVerifyMessageTest { assertTrue(metadata.containsVerifiedSignatureFrom(TestKeys.JULIET_FINGERPRINT)); assertEquals(new SubkeyIdentifier(TestKeys.JULIET_FINGERPRINT), metadata.getDecryptionKey()); } + + @Test + public void testDecryptMessageWithUnacceptableSymmetricAlgorithm() { + String ciphertext = "-----BEGIN PGP MESSAGE-----\n" + + "Version: PGPainless\n" + + "\n" + + "jA0EAQMCZv8glrLeXPhg0jgBpMN+E8dCuEDxJnSi8/e+HOKcdYQbgQh/MG4Kn7NK\n" + + "wRM5wNOFKn8jbsoC+JalzjwzMJSV+ZM1aQ==\n" + + "=9aCQ\n" + + "-----END PGP MESSAGE-----"; + ByteArrayInputStream ciphertextIn = new ByteArrayInputStream(ciphertext.getBytes()); + assertThrows(MissingDecryptionMethodException.class, + () -> PGPainless.decryptAndOrVerify() + .onInputStream(ciphertextIn) + .withOptions(ConsumerOptions.get() + .addDecryptionPassphrase(Passphrase.fromPassword("sw0rdf1sh")))); + } } From 68886613a6c0c28a79b411d2e9b230c794a317e4 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 24 Nov 2022 22:14:06 +0100 Subject: [PATCH 159/763] SOP KeyReader: wrap IOException in BadData --- .../src/main/java/org/pgpainless/sop/KeyReader.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/KeyReader.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/KeyReader.java index 064a5e39..5e6f3a7d 100644 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/KeyReader.java +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/KeyReader.java @@ -31,7 +31,7 @@ class KeyReader { } throw e; } catch (PGPException e) { - throw new IOException("Cannot read keys.", e); + throw new SOPGPException.BadData("Cannot read keys.", e); } if (requireContent && (keys == null || keys.size() == 0)) { @@ -41,7 +41,8 @@ class KeyReader { return keys; } - static PGPPublicKeyRingCollection readPublicKeys(InputStream certIn, boolean requireContent) throws IOException { + static PGPPublicKeyRingCollection readPublicKeys(InputStream certIn, boolean requireContent) + throws IOException { PGPPublicKeyRingCollection certs; try { certs = PGPainless.readKeyRing().publicKeyRingCollection(certIn); From 9919bbf0136b2a835ef564aba94589115962b7bc Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 24 Nov 2022 22:20:02 +0100 Subject: [PATCH 160/763] Enable test for reading broken keys in SOP --- .../test/java/org/pgpainless/cli/commands/DearmorCmdTest.java | 3 --- 1 file changed, 3 deletions(-) diff --git a/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/DearmorCmdTest.java b/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/DearmorCmdTest.java index 64576bfe..7ebb7308 100644 --- a/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/DearmorCmdTest.java +++ b/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/DearmorCmdTest.java @@ -13,7 +13,6 @@ import java.io.IOException; import org.bouncycastle.openpgp.PGPPublicKeyRing; import org.bouncycastle.openpgp.PGPSecretKeyRing; -import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.pgpainless.PGPainless; import org.slf4j.LoggerFactory; @@ -62,8 +61,6 @@ public class DearmorCmdTest extends CLITest { } @Test - @Disabled("Enable with SOP-Java 4.0.7") - // TODO: Enable public void dearmorBrokenArmoredKeyFails() throws IOException { // contains a "-" String invalidBase64 = "lFgEY2vOkhYJKwYBBAHaRw8BAQdAqGOtLd1tKnuwaYYcdr2/7C0cPiCCggRMKG+Wt32QQdEAAP9VaBzjk/AaAqyykZnQHmS1HByEvRLv5/4yJMSr22451BFjtBRhbGljZUBwZ3BhaW5sZXNzLm9yZ4iOBBMWCgBBBQJja86SCRCLB1F3AflTTBYhBGLp3aTyD4NB0rxLT-IsHUXcB+VNMAp4BApsBBRYCAwEABAsJCAcFFQoJCAsCmQEAACZhAP4s8hn/RBDvyLvGROOd15EYATnWlgyi+b5WXP6cELalJwD1FZy3RROhfNtZWcJPS43fG03pYNyb0NXoitIMAaXEB5xdBGNrzpISCisGAQQBl1UBBQEBB0CqCcYethOynfni8uRO+r/cZWp9hCLy8pRIExKqzcyEFAMBCAcAAP9sRRLoZkLpDaTNNrtIBovXu2ANhL8keUMWtVcuEHnkQA6iiHUEGBYKAB0FAmNrzpICngECmwwFFgIDAQAECwkIBwUVCgkICwAKCRCLB1F3AflTTBVpAP491etrjqCMWx2bBaw3K1vP0Mix6U0vF3J4kP9UeZm6owEA4kX9VAGESvLgIc7CEiswmxdWjxnLQyCRtWXfjgFmYQucWARja86SFgkrBgEEAdpHDwEBB0DBslhDpWC6CV3xJUSo071NSO5Cf4fgOwOj+QHs8mpFbwABAPkQioSydYiMi04LyfPohyrhhcdJDHallQg+jYHHUb2pEJCI1QQYFgoAfQUCY2vOkgKeAQKbAgUWAgMBAAQLCQgHBRUKCQgLXyAEGRYKAAYFAmNrzpIACgkQiHlkvEXh+f1eywEA9A2GLU9LxCJxZf2X4qcZY//YJDChIZHPnY0Vaek1DsMBAN1YILrH2rxQeCXjm4bUKfJIRrGt6ZJscwORgNI1dFQFAAoJEIsHUXcB+VNMK3gA/3vvPm57JsHA860wlB4D1II71oFNL8TFnJqTAvpSKe1AAP49S4mKB4PE0ElcDo7n+nEYt6ba8IMRDlMorsH85mUgCw=="; From 39d656d2dd86ccf758bf32b1e2d8a8ad5b9a979d Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 24 Nov 2022 22:22:21 +0100 Subject: [PATCH 161/763] Add javadoc for HardwareDataDecryptorFactory constructor argument --- .../org/pgpainless/decryption_verification/HardwareSecurity.java | 1 + 1 file changed, 1 insertion(+) diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/HardwareSecurity.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/HardwareSecurity.java index daf902d4..cdadf2c4 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/HardwareSecurity.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/HardwareSecurity.java @@ -51,6 +51,7 @@ public class HardwareSecurity { /** * Create a new {@link HardwareDataDecryptorFactory}. * + * @param subkeyIdentifier identifier of the decryption subkey * @param callback decryption callback */ public HardwareDataDecryptorFactory(SubkeyIdentifier subkeyIdentifier, DecryptionCallback callback) { From e88a88a447e78c3ee7bc5bea703eed1fffbde6d2 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 24 Nov 2022 22:24:12 +0100 Subject: [PATCH 162/763] Add javadoc for OpenPgpMessageInputStream factory method return value --- .../decryption_verification/OpenPgpMessageInputStream.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java index 263871de..c26c949b 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java @@ -105,6 +105,7 @@ public class OpenPgpMessageInputStream extends DecryptionStream { * * @param inputStream underlying input stream * @param options options for consuming the stream + * @return input stream that consumes OpenPGP messages * * @throws IOException in case of an IO error * @throws PGPException in case of an OpenPGP error @@ -123,6 +124,7 @@ public class OpenPgpMessageInputStream extends DecryptionStream { * @param inputStream underlying input stream containing the OpenPGP message * @param options options for consuming the message * @param policy policy for acceptable algorithms etc. + * @return input stream that consumes OpenPGP messages * * @throws PGPException in case of an OpenPGP error * @throws IOException in case of an IO error From ce049bf9a42b8a70cc6c1e9acba2effefab989de Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 24 Nov 2022 22:25:27 +0100 Subject: [PATCH 163/763] PGPainless 1.4.0-rc2 --- CHANGELOG.md | 2 +- README.md | 2 +- pgpainless-sop/README.md | 4 ++-- version.gradle | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9fc7e655..0a335092 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ SPDX-License-Identifier: CC0-1.0 # PGPainless Changelog -## 1.4.0-rc2-SNAPSHOT +## 1.4.0-rc2 - Bump `bcpg-jdk15to18` to `1.72.3` - Use BCs `PGPEncryptedDataList.extractSessionKeyEncryptedData()` method to do decryption using session keys. This enables decryption of messages diff --git a/README.md b/README.md index 1855f742..30af42f8 100644 --- a/README.md +++ b/README.md @@ -191,7 +191,7 @@ repositories { } dependencies { - implementation 'org.pgpainless:pgpainless-core:1.4.0-rc1' + implementation 'org.pgpainless:pgpainless-core:1.4.0-rc2' } ``` diff --git a/pgpainless-sop/README.md b/pgpainless-sop/README.md index 893804dd..2e4927a6 100644 --- a/pgpainless-sop/README.md +++ b/pgpainless-sop/README.md @@ -23,7 +23,7 @@ To start using pgpainless-sop in your code, include the following lines in your ... dependencies { ... - implementation "org.pgpainless:pgpainless-sop:1.4.0-rc1" + implementation "org.pgpainless:pgpainless-sop:1.4.0-rc2" ... } @@ -34,7 +34,7 @@ dependencies { org.pgpainless pgpainless-sop - 1.4.0-rc1 + 1.4.0-rc2 ... diff --git a/version.gradle b/version.gradle index 86fc8f54..aee46cd8 100644 --- a/version.gradle +++ b/version.gradle @@ -5,7 +5,7 @@ allprojects { ext { shortVersion = '1.4.0-rc2' - isSnapshot = true + isSnapshot = false pgpainlessMinAndroidSdk = 10 javaSourceCompatibility = 1.8 bouncyCastleVersion = '1.72' From c8c93594853a881e527624e8d8044cc5e94c96da Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 24 Nov 2022 22:28:15 +0100 Subject: [PATCH 164/763] PGPainless 1.4.0-rc3-SNAPSHOT --- version.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/version.gradle b/version.gradle index aee46cd8..2d9745dd 100644 --- a/version.gradle +++ b/version.gradle @@ -4,8 +4,8 @@ allprojects { ext { - shortVersion = '1.4.0-rc2' - isSnapshot = false + shortVersion = '1.4.0-rc3' + isSnapshot = true pgpainlessMinAndroidSdk = 10 javaSourceCompatibility = 1.8 bouncyCastleVersion = '1.72' From 3f70936ff158c7d0c0b884751cb2d0ad38541b55 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Fri, 25 Nov 2022 14:26:55 +0100 Subject: [PATCH 165/763] Add documetation to PDA class --- .../syntax_check/PDA.java | 39 +++++++++++++++++-- 1 file changed, 36 insertions(+), 3 deletions(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/PDA.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/PDA.java index adc5bb39..7d7cf973 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/PDA.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/PDA.java @@ -9,6 +9,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.annotation.Nonnull; +import javax.annotation.Nullable; import java.util.ArrayList; import java.util.Arrays; import java.util.List; @@ -17,6 +18,12 @@ import java.util.Stack; import static org.pgpainless.decryption_verification.syntax_check.StackSymbol.msg; import static org.pgpainless.decryption_verification.syntax_check.StackSymbol.terminus; +/** + * Pushdown Automaton for validating context-free languages. + * In PGPainless, this class is used to validate OpenPGP message packet sequences against the allowed syntax. + * + * @see OpenPGP Message Syntax + */ public class PDA { private static final Logger LOGGER = LoggerFactory.getLogger(PDA.class); @@ -36,6 +43,13 @@ public class PDA { this(new OpenPgpMessageSyntax(), State.OpenPgpMessage, terminus, msg); } + /** + * Construct a PDA with a custom {@link Syntax}, initial {@link State} and initial {@link StackSymbol StackSymbols}. + * + * @param syntax syntax + * @param initialState initial state + * @param initialStack zero or more initial stack items (get pushed onto the stack in order of appearance) + */ public PDA(@Nonnull Syntax syntax, @Nonnull State initialState, @Nonnull StackSymbol... initialStack) { this.syntax = syntax; this.state = initialState; @@ -44,7 +58,16 @@ public class PDA { } } - public void next(InputSymbol input) throws MalformedOpenPgpMessageException { + /** + * Process the next {@link InputSymbol}. + * This will either leave the PDA in the next state, or throw a {@link MalformedOpenPgpMessageException} if the + * input symbol is rejected. + * + * @param input input symbol + * @throws MalformedOpenPgpMessageException if the input symbol is rejected + */ + public void next(@Nonnull InputSymbol input) + throws MalformedOpenPgpMessageException { StackSymbol stackSymbol = popStack(); try { Transition transition = syntax.transition(state, input, stackSymbol); @@ -69,11 +92,16 @@ public class PDA { * * @return state */ - public State getState() { + public @Nonnull State getState() { return state; } - public StackSymbol peekStack() { + /** + * Peek at the stack, returning the topmost stack item without changing the stack. + * + * @return topmost stack item, or null if stack is empty + */ + public @Nullable StackSymbol peekStack() { if (stack.isEmpty()) { return null; } @@ -89,6 +117,11 @@ public class PDA { return getState() == State.Valid && stack.isEmpty(); } + /** + * Throw a {@link MalformedOpenPgpMessageException} if the pda is not in a valid state right now. + * + * @throws MalformedOpenPgpMessageException if the pda is not in an acceptable state + */ public void assertValid() throws MalformedOpenPgpMessageException { if (!isValid()) { throw new MalformedOpenPgpMessageException("Pushdown Automaton is not in an acceptable state: " + toString()); From 7cc27515272f192fab678bbd32c9fc79bd0bbe27 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Fri, 25 Nov 2022 14:38:45 +0100 Subject: [PATCH 166/763] Add @Nonnull annotations to OpenPgpMessageSyntax --- .../syntax_check/OpenPgpMessageSyntax.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/OpenPgpMessageSyntax.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/OpenPgpMessageSyntax.java index c6de8765..6abb507a 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/OpenPgpMessageSyntax.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/OpenPgpMessageSyntax.java @@ -40,6 +40,7 @@ public class OpenPgpMessageSyntax implements Syntax { throw new MalformedOpenPgpMessageException(from, input, stackItem); } + @Nonnull Transition fromOpenPgpMessage(@Nonnull InputSymbol input, @Nullable StackSymbol stackItem) throws MalformedOpenPgpMessageException { if (stackItem != StackSymbol.msg) { @@ -68,6 +69,7 @@ public class OpenPgpMessageSyntax implements Syntax { } } + @Nonnull Transition fromLiteralMessage(@Nonnull InputSymbol input, @Nullable StackSymbol stackItem) throws MalformedOpenPgpMessageException { switch (input) { @@ -87,6 +89,7 @@ public class OpenPgpMessageSyntax implements Syntax { throw new MalformedOpenPgpMessageException(State.LiteralMessage, input, stackItem); } + @Nonnull Transition fromCompressedMessage(@Nonnull InputSymbol input, @Nullable StackSymbol stackItem) throws MalformedOpenPgpMessageException { switch (input) { @@ -106,6 +109,7 @@ public class OpenPgpMessageSyntax implements Syntax { throw new MalformedOpenPgpMessageException(State.CompressedMessage, input, stackItem); } + @Nonnull Transition fromEncryptedMessage(@Nonnull InputSymbol input, @Nullable StackSymbol stackItem) throws MalformedOpenPgpMessageException { switch (input) { @@ -125,6 +129,7 @@ public class OpenPgpMessageSyntax implements Syntax { throw new MalformedOpenPgpMessageException(State.EncryptedMessage, input, stackItem); } + @Nonnull Transition fromValid(@Nonnull InputSymbol input, @Nullable StackSymbol stackItem) throws MalformedOpenPgpMessageException { // There is no applicable transition rule out of Valid From e1ab128c2e01db597d4f584dc3555a89b3c2d810 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Fri, 25 Nov 2022 14:40:57 +0100 Subject: [PATCH 167/763] Add annotations to GnuPGDummyKeyUtil --- .../java/org/gnupg/GnuPGDummyKeyUtil.java | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/pgpainless-core/src/main/java/org/gnupg/GnuPGDummyKeyUtil.java b/pgpainless-core/src/main/java/org/gnupg/GnuPGDummyKeyUtil.java index 983d8e68..42af92d8 100644 --- a/pgpainless-core/src/main/java/org/gnupg/GnuPGDummyKeyUtil.java +++ b/pgpainless-core/src/main/java/org/gnupg/GnuPGDummyKeyUtil.java @@ -13,6 +13,7 @@ import org.bouncycastle.openpgp.PGPSecretKeyRing; import org.pgpainless.key.SubkeyIdentifier; import javax.annotation.Nonnull; +import javax.annotation.Nullable; import java.util.ArrayList; import java.util.Collection; import java.util.HashSet; @@ -40,7 +41,7 @@ public final class GnuPGDummyKeyUtil { * @param secretKeys secret keys * @return set of keys with S2K type GNU_DUMMY_S2K and protection mode DIVERT_TO_CARD */ - public static Set getIdsOfKeysWithGnuPGS2KDivertedToCard(PGPSecretKeyRing secretKeys) { + public static Set getIdsOfKeysWithGnuPGS2KDivertedToCard(@Nonnull PGPSecretKeyRing secretKeys) { Set hardwareBackedKeys = new HashSet<>(); for (PGPSecretKey secretKey : secretKeys) { S2K s2K = secretKey.getS2K(); @@ -65,7 +66,7 @@ public final class GnuPGDummyKeyUtil { * @param secretKeys secret keys * @return builder */ - public static Builder modify(PGPSecretKeyRing secretKeys) { + public static Builder modify(@Nonnull PGPSecretKeyRing secretKeys) { return new Builder(secretKeys); } @@ -73,7 +74,7 @@ public final class GnuPGDummyKeyUtil { private final PGPSecretKeyRing keys; - private Builder(PGPSecretKeyRing keys) { + private Builder(@Nonnull PGPSecretKeyRing keys) { this.keys = keys; } @@ -84,7 +85,7 @@ public final class GnuPGDummyKeyUtil { * @param filter filter to select keys for removal * @return modified key ring */ - public PGPSecretKeyRing removePrivateKeys(KeyFilter filter) { + public PGPSecretKeyRing removePrivateKeys(@Nonnull KeyFilter filter) { return replacePrivateKeys(GnuPGDummyExtension.NO_PRIVATE_KEY, null, filter); } @@ -92,13 +93,12 @@ public final class GnuPGDummyKeyUtil { * Remove all private keys that match the given {@link KeyFilter} from the key ring and replace them with * GNU_DUMMY keys with S2K protection mode {@link GnuPGDummyExtension#DIVERT_TO_CARD}. * This method will set the serial number of the card to 0x00000000000000000000000000000000. - * * NOTE: This method does not actually move any keys to a card. * * @param filter filter to select keys for removal * @return modified key ring */ - public PGPSecretKeyRing divertPrivateKeysToCard(KeyFilter filter) { + public PGPSecretKeyRing divertPrivateKeysToCard(@Nonnull KeyFilter filter) { return divertPrivateKeysToCard(filter, new byte[16]); } @@ -106,21 +106,22 @@ public final class GnuPGDummyKeyUtil { * Remove all private keys that match the given {@link KeyFilter} from the key ring and replace them with * GNU_DUMMY keys with S2K protection mode {@link GnuPGDummyExtension#DIVERT_TO_CARD}. * This method will include the card serial number into the encoded dummy key. - * * NOTE: This method does not actually move any keys to a card. * * @param filter filter to select keys for removal * @param cardSerialNumber serial number of the card (at most 16 bytes long) * @return modified key ring */ - public PGPSecretKeyRing divertPrivateKeysToCard(KeyFilter filter, byte[] cardSerialNumber) { + public PGPSecretKeyRing divertPrivateKeysToCard(@Nonnull KeyFilter filter, @Nullable byte[] cardSerialNumber) { if (cardSerialNumber != null && cardSerialNumber.length > 16) { throw new IllegalArgumentException("Card serial number length cannot exceed 16 bytes."); } return replacePrivateKeys(GnuPGDummyExtension.DIVERT_TO_CARD, cardSerialNumber, filter); } - private PGPSecretKeyRing replacePrivateKeys(GnuPGDummyExtension extension, byte[] serial, KeyFilter filter) { + private PGPSecretKeyRing replacePrivateKeys(@Nonnull GnuPGDummyExtension extension, + @Nullable byte[] serial, + @Nonnull KeyFilter filter) { byte[] encodedSerial = serial != null ? encodeSerial(serial) : null; S2K s2k = extensionToS2K(extension); From 4426895814952b6ec10e4e7a9a49b2dd358dd200 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Fri, 25 Nov 2022 14:55:46 +0100 Subject: [PATCH 168/763] Add tests for CollectionUtils --- .../pgpainless/util/CollectionUtilsTest.java | 112 ++++++++++++++++++ 1 file changed, 112 insertions(+) create mode 100644 pgpainless-core/src/test/java/org/pgpainless/util/CollectionUtilsTest.java diff --git a/pgpainless-core/src/test/java/org/pgpainless/util/CollectionUtilsTest.java b/pgpainless-core/src/test/java/org/pgpainless/util/CollectionUtilsTest.java new file mode 100644 index 00000000..2bfe6cb1 --- /dev/null +++ b/pgpainless-core/src/test/java/org/pgpainless/util/CollectionUtilsTest.java @@ -0,0 +1,112 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.util; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; + +import org.junit.jupiter.api.Test; + +public class CollectionUtilsTest { + + @Test + public void testConcat() { + String a = "A"; + String[] bc = new String[] {"B", "C"}; + + String[] abc = CollectionUtils.concat(a, bc); + assertArrayEquals(new String[] {"A", "B", "C"}, abc); + } + + @Test + public void testConcatWithEmptyArray() { + String a = "A"; + String[] empty = new String[0]; + + String[] concat = CollectionUtils.concat(a, empty); + assertArrayEquals(new String[] {"A"}, concat); + } + + @Test + public void iteratorToListTest() { + List list = Arrays.asList("A", "B", "C"); + Iterator iterator = list.iterator(); + + List listFromIterator = CollectionUtils.iteratorToList(iterator); + assertEquals(list, listFromIterator); + } + + @Test + public void iteratorToList_emptyIteratorTest() { + Iterator iterator = Collections.emptyIterator(); + + List listFromIterator = CollectionUtils.iteratorToList(iterator); + assertTrue(listFromIterator.isEmpty()); + } + + @Test + public void containsTest() { + String[] abc = new String[] {"A", "B", "C"}; + + assertTrue(CollectionUtils.contains(abc, "A")); + assertTrue(CollectionUtils.contains(abc, "B")); + assertTrue(CollectionUtils.contains(abc, "C")); + assertFalse(CollectionUtils.contains(abc, "D")); + } + + @Test + public void contains_emptyTest() { + String[] empty = new String[0]; + + assertFalse(CollectionUtils.contains(empty, "A")); + } + + @Test + public void addAllTest() { + List list = new ArrayList<>(); + list.add("A"); + list.add("B"); + + List other = new ArrayList<>(); + other.add("C"); + other.add("D"); + Iterator iterator = other.iterator(); + + CollectionUtils.addAll(iterator, list); + + assertEquals(Arrays.asList("A", "B", "C", "D"), list); + } + + @Test + public void addAllEmptyListTest() { + List empty = new ArrayList<>(); + + List other = Arrays.asList("A", "B", "C"); + Iterator iterator = other.iterator(); + + CollectionUtils.addAll(iterator, empty); + assertEquals(Arrays.asList("A", "B", "C"), empty); + } + + @Test + public void addAllEmptyIterator() { + List list = new ArrayList<>(); + list.add("A"); + list.add("B"); + + Iterator iterator = Collections.emptyIterator(); + + CollectionUtils.addAll(iterator, list); + assertEquals(Arrays.asList("A", "B"), list); + } +} From ae6a427d90a29aa030ee4cc8a96b9316c0004192 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Fri, 25 Nov 2022 15:34:54 +0100 Subject: [PATCH 169/763] Add test for UniversalSignatureBuilder --- .../builder/UniversalSignatureBuilder.java | 3 +- .../subpackets/SignatureSubpackets.java | 4 + .../UniversalSignatureBuilderTest.java | 90 +++++++++++++++++++ 3 files changed, 95 insertions(+), 2 deletions(-) create mode 100644 pgpainless-core/src/test/java/org/pgpainless/signature/builder/UniversalSignatureBuilderTest.java diff --git a/pgpainless-core/src/main/java/org/pgpainless/signature/builder/UniversalSignatureBuilder.java b/pgpainless-core/src/main/java/org/pgpainless/signature/builder/UniversalSignatureBuilder.java index b3ed5bf0..7ca512bf 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/signature/builder/UniversalSignatureBuilder.java +++ b/pgpainless-core/src/main/java/org/pgpainless/signature/builder/UniversalSignatureBuilder.java @@ -12,7 +12,6 @@ import org.bouncycastle.openpgp.PGPSignature; import org.bouncycastle.openpgp.PGPSignatureGenerator; import org.pgpainless.algorithm.SignatureType; import org.pgpainless.key.protection.SecretKeyRingProtector; -import org.pgpainless.signature.subpackets.BaseSignatureSubpackets; import org.pgpainless.signature.subpackets.SignatureSubpackets; /** @@ -43,7 +42,7 @@ public class UniversalSignatureBuilder extends AbstractSignatureBuilder { + + } + public static SignatureSubpackets refreshHashedSubpackets(PGPPublicKey issuer, PGPSignature oldSignature) { return createHashedSubpacketsFrom(issuer, oldSignature.getHashedSubPackets()); } diff --git a/pgpainless-core/src/test/java/org/pgpainless/signature/builder/UniversalSignatureBuilderTest.java b/pgpainless-core/src/test/java/org/pgpainless/signature/builder/UniversalSignatureBuilderTest.java new file mode 100644 index 00000000..37bc6fd3 --- /dev/null +++ b/pgpainless-core/src/test/java/org/pgpainless/signature/builder/UniversalSignatureBuilderTest.java @@ -0,0 +1,90 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.signature.builder; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; + +import java.io.IOException; + +import org.bouncycastle.bcpg.sig.PrimaryUserID; +import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.PGPSecretKey; +import org.bouncycastle.openpgp.PGPSecretKeyRing; +import org.bouncycastle.openpgp.PGPSignature; +import org.bouncycastle.openpgp.PGPSignatureGenerator; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.pgpainless.PGPainless; +import org.pgpainless.algorithm.HashAlgorithm; +import org.pgpainless.algorithm.KeyFlag; +import org.pgpainless.algorithm.SignatureType; +import org.pgpainless.key.protection.SecretKeyRingProtector; +import org.pgpainless.signature.subpackets.SignatureSubpackets; + +public class UniversalSignatureBuilderTest { + + private static final String KEY = "-----BEGIN PGP PRIVATE KEY BLOCK-----\n" + + "Version: PGPainless\n" + + "Comment: 9611 510F 313E DBC2 BBBC DC24 3BAD F1F8 3E70 DC34\n" + + "Comment: Signora Universa \n" + + "\n" + + "lFgEY4DKKRYJKwYBBAHaRw8BAQdA65vJxvvLASI/gczDP8ZKH4C+16MLU7F5iP91\n" + + "8WWUqM0AAQCRSTHLLQWT9tuNRgkG3xaIiBGkEGD7Ou/R3oga6tc1MA8UtClTaWdu\n" + + "b3JhIFVuaXZlcnNhIDxzaWdub3JhQHBncGFpbmxlc3Mub3JnPoiPBBMWCgBBBQJj\n" + + "gMopCRA7rfH4PnDcNBYhBJYRUQ8xPtvCu7zcJDut8fg+cNw0Ap4BApsBBRYCAwEA\n" + + "BAsJCAcFFQoJCAsCmQEAAOgMAPwIOXWt3EBBusK5Ps3m7p/5HsecZv3IXtscEQBx\n" + + "vKlULwD/YuLP1XJSqcE2cQJRNt6OLi9Nt02MKBYkhWrRCYZAcQicXQRjgMopEgor\n" + + "BgEEAZdVAQUBAQdAWTstuhvHwmSXaQ4Vh8yxl0DZcvjrWkZI+n9/uFBxEmoDAQgH\n" + + "AAD/eRt6kgOMzWsTuM00am4UhSygxmDt7h6JkBTnpyyhK0gPiYh1BBgWCgAdBQJj\n" + + "gMopAp4BApsMBRYCAwEABAsJCAcFFQoJCAsACgkQO63x+D5w3DRnZAEA6GlS9Tw8\n" + + "9SJlUvh5aciYSlQUplnEdng+Pvzbj74zcXIA/2OkyMN428ddNhkHWWkZCMOxApum\n" + + "/zNDSYMwvByQ2KcFnFgEY4DKKRYJKwYBBAHaRw8BAQdAfhPrtVuG3g/zXF51VrPv\n" + + "kpQQk9aqjrkBMI0qlztBpu0AAP9Mw7NCsAVwg9CgmSzG2ATIDp3yf/4BGVYDs7qu\n" + + "+sbn7xKIiNUEGBYKAH0FAmOAyikCngECmwIFFgIDAQAECwkIBwUVCgkIC18gBBkW\n" + + "CgAGBQJjgMopAAoJENmzwZA/hq5ZCqIBAMYeOnASBd+WWta7Teh3g7Bl7sFY42Qy\n" + + "0OnaSGk/pLm9AP4yC62Xpb9DhWeiQIOY7k5n4lhNn173IfzDK6KXzBKkBgAKCRA7\n" + + "rfH4PnDcNMInAP4oanG9tbuczBNLN3JY4Hg4AaB+w5kfdOJxKwnAw7U0cgEAtasg\n" + + "67qSjHvsEvjNKeXzUm+db7NWP3fpIHxAmjWVjwM=\n" + + "=Dqbd\n" + + "-----END PGP PRIVATE KEY BLOCK-----"; + + private PGPSecretKeyRing secretKeys; + private final SecretKeyRingProtector protector = SecretKeyRingProtector.unprotectedKeys(); + + @BeforeEach + public void parseKey() throws IOException { + secretKeys = PGPainless.readKeyRing().secretKeyRing(KEY); + } + + @Test + public void createPetNameSignature() throws PGPException { + PGPSecretKey signingKey = secretKeys.getSecretKey(); + PGPSignature archetype = signingKey.getPublicKey().getSignatures().next(); + UniversalSignatureBuilder builder = new UniversalSignatureBuilder( + signingKey, protector, archetype); + + builder.applyCallback(new SignatureSubpackets.Callback() { + @Override + public void modifyHashedSubpackets(SignatureSubpackets hashedSubpackets) { + hashedSubpackets.setExportable(true, false); + hashedSubpackets.setPrimaryUserId(new PrimaryUserID(false, false)); + } + }); + + PGPSignatureGenerator generator = builder.getSignatureGenerator(); + + String petName = "mykey"; + PGPSignature petNameSig = generator.generateCertification(petName, secretKeys.getPublicKey()); + + assertEquals(SignatureType.POSITIVE_CERTIFICATION.getCode(), petNameSig.getSignatureType()); + assertEquals(4, petNameSig.getVersion()); + assertEquals(signingKey.getKeyID(), petNameSig.getKeyID()); + assertEquals(HashAlgorithm.SHA512.getAlgorithmId(), petNameSig.getHashAlgorithm()); + assertEquals(KeyFlag.toBitmask(KeyFlag.CERTIFY_OTHER), petNameSig.getHashedSubPackets().getKeyFlags()); + assertFalse(petNameSig.getHashedSubPackets().isExportable()); + assertFalse(petNameSig.getHashedSubPackets().isPrimaryUserID()); + } +} From 6913aa3d6dce6566715c12ffdca4f7863691ba7d Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Fri, 25 Nov 2022 15:41:56 +0100 Subject: [PATCH 170/763] Add more tests for RevocationState --- .../pgpainless/algorithm/RevocationStateTest.java | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/pgpainless-core/src/test/java/org/pgpainless/algorithm/RevocationStateTest.java b/pgpainless-core/src/test/java/org/pgpainless/algorithm/RevocationStateTest.java index c1e45413..e23d92d3 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/algorithm/RevocationStateTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/algorithm/RevocationStateTest.java @@ -72,6 +72,8 @@ public class RevocationStateTest { Date now = new Date(); assertEquals(RevocationState.softRevoked(now), RevocationState.softRevoked(now)); + assertEquals(1, RevocationState.softRevoked(now).compareTo(RevocationState.notRevoked())); + assertEquals(0, RevocationState.notRevoked().compareTo(RevocationState.notRevoked())); assertEquals(0, RevocationState.hardRevoked().compareTo(RevocationState.hardRevoked())); assertTrue(RevocationState.hardRevoked().compareTo(RevocationState.notRevoked()) > 0); @@ -93,4 +95,15 @@ public class RevocationStateTest { assertEquals(states, Arrays.asList(not, not2, laterSoft, earlySoft, hard)); } + + @SuppressWarnings({"SimplifiableAssertion", "ConstantConditions", "EqualsWithItself", "EqualsBetweenInconvertibleTypes"}) + @Test + public void equalsTest() { + RevocationState rev = RevocationState.hardRevoked(); + assertFalse(rev.equals(null)); + assertTrue(rev.equals(rev)); + assertFalse(rev.equals("not a revocation")); + RevocationState other = RevocationState.notRevoked(); + assertFalse(rev.equals(other)); + } } From b0c283e1435f859a5d23e1b656bc512a90f19638 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 30 Nov 2022 15:34:04 +0100 Subject: [PATCH 171/763] Clean up UserId.toString() behavior --- .../java/org/pgpainless/key/util/UserId.java | 37 ++++++++++--------- .../java/org/pgpainless/key/UserIdTest.java | 10 ++--- .../GenerateKeyWithAdditionalUserIdTest.java | 6 +-- 3 files changed, 26 insertions(+), 27 deletions(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/util/UserId.java b/pgpainless-core/src/main/java/org/pgpainless/key/util/UserId.java index ca233759..25db7e39 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/key/util/UserId.java +++ b/pgpainless-core/src/main/java/org/pgpainless/key/util/UserId.java @@ -5,6 +5,7 @@ package org.pgpainless.key.util; import javax.annotation.Nonnull; +import javax.annotation.Nullable; public final class UserId implements CharSequence { public static final class Builder { @@ -64,18 +65,18 @@ public final class UserId implements CharSequence { private final String email; private long hash = Long.MAX_VALUE; - private UserId(String name, String comment, String email) { - this.name = name; - this.comment = comment; - this.email = email; + private UserId(@Nullable String name, @Nullable String comment, @Nullable String email) { + this.name = name == null ? null : name.trim(); + this.comment = comment == null ? null : comment.trim(); + this.email = email == null ? null : email.trim(); } - public static UserId onlyEmail(String email) { + public static UserId onlyEmail(@Nonnull String email) { checkNotNull("email", email); return new UserId(null, null, email); } - public static UserId nameAndEmail(String name, String email) { + public static UserId nameAndEmail(@Nonnull String name, @Nonnull String email) { checkNotNull("name", name); checkNotNull("email", email); return new UserId(name, null, email); @@ -118,27 +119,29 @@ public final class UserId implements CharSequence { @Override public @Nonnull String toString() { - return asString(false); + return asString(); } /** * Returns a string representation of the object. - * @param ignoreEmptyValues Flag which indicates that empty string values should not be outputted. * @return a string representation of the object. */ - public String asString(boolean ignoreEmptyValues) { + public String asString() { StringBuilder sb = new StringBuilder(); - if (name != null && (!ignoreEmptyValues || !name.isEmpty())) { + if (name != null && !name.isEmpty()) { sb.append(name); } - if (comment != null && (!ignoreEmptyValues || !comment.isEmpty())) { - sb.append(" (").append(comment).append(')'); + if (comment != null && !comment.isEmpty()) { + if (sb.length() > 0) { + sb.append(' '); + } + sb.append('(').append(comment).append(')'); } - if (email != null && (!ignoreEmptyValues || !email.isEmpty())) { - final boolean moreThanJustEmail = sb.length() > 0; - if (moreThanJustEmail) sb.append(" <"); - sb.append(email); - if (moreThanJustEmail) sb.append('>'); + if (email != null && !email.isEmpty()) { + if (sb.length() > 0) { + sb.append(' '); + } + sb.append('<').append(email).append('>'); } return sb.toString(); } diff --git a/pgpainless-core/src/test/java/org/pgpainless/key/UserIdTest.java b/pgpainless-core/src/test/java/org/pgpainless/key/UserIdTest.java index 4e596b3f..32d56eff 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/key/UserIdTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/key/UserIdTest.java @@ -151,15 +151,13 @@ public class UserIdTest { @Test void testEmailOnlyFormatting() { final UserId userId = UserId.onlyEmail("john.smith@example.com"); - assertEquals("john.smith@example.com", userId.toString()); + assertEquals("", userId.toString()); } @Test void testEmptyNameAndValidEmailFormatting() { final UserId userId = UserId.nameAndEmail("", "john.smith@example.com"); - assertEquals("john.smith@example.com", userId.toString()); - assertEquals("john.smith@example.com", userId.asString(false)); - assertEquals("john.smith@example.com", userId.asString(true)); + assertEquals("", userId.toString()); } @Test @@ -169,9 +167,7 @@ public class UserIdTest { .withName("") .withEmail("john.smith@example.com") .build(); - assertEquals(" () ", userId.toString()); - assertEquals(" () ", userId.asString(false)); - assertEquals("john.smith@example.com", userId.asString(true)); + assertEquals("", userId.toString()); } @Test diff --git a/pgpainless-core/src/test/java/org/pgpainless/key/generation/GenerateKeyWithAdditionalUserIdTest.java b/pgpainless-core/src/test/java/org/pgpainless/key/generation/GenerateKeyWithAdditionalUserIdTest.java index e68d8e9b..cf12ab57 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/key/generation/GenerateKeyWithAdditionalUserIdTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/key/generation/GenerateKeyWithAdditionalUserIdTest.java @@ -51,9 +51,9 @@ public class GenerateKeyWithAdditionalUserIdTest { JUtils.assertDateEquals(expiration, PGPainless.inspectKeyRing(publicKeys).getPrimaryKeyExpirationDate()); Iterator userIds = publicKeys.getPublicKey().getUserIDs(); - assertEquals("primary@user.id", userIds.next()); - assertEquals("additional@user.id", userIds.next()); - assertEquals("additional2@user.id", userIds.next()); + assertEquals("", userIds.next()); + assertEquals("", userIds.next()); + assertEquals("", userIds.next()); assertEquals("trimThis@user.id", userIds.next()); assertFalse(userIds.hasNext()); } From 4c1d359971106124f2c0c68eb61836a8886d3651 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 30 Nov 2022 15:35:31 +0100 Subject: [PATCH 172/763] Deprecate UserId.asString() --- .../java/org/pgpainless/key/util/UserId.java | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/util/UserId.java b/pgpainless-core/src/main/java/org/pgpainless/key/util/UserId.java index 25db7e39..3ac3d77a 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/key/util/UserId.java +++ b/pgpainless-core/src/main/java/org/pgpainless/key/util/UserId.java @@ -119,14 +119,6 @@ public final class UserId implements CharSequence { @Override public @Nonnull String toString() { - return asString(); - } - - /** - * Returns a string representation of the object. - * @return a string representation of the object. - */ - public String asString() { StringBuilder sb = new StringBuilder(); if (name != null && !name.isEmpty()) { sb.append(name); @@ -146,6 +138,16 @@ public final class UserId implements CharSequence { return sb.toString(); } + /** + * Returns a string representation of the object. + * @return a string representation of the object. + * @deprecated use {@link #toString()} instead. + */ + @Deprecated + public String asString() { + return toString(); + } + @Override public boolean equals(Object o) { if (o == null) return false; From 837fbd36355008fa81c263c510e9201b9752c02e Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 30 Nov 2022 15:41:53 +0100 Subject: [PATCH 173/763] Simplify UserIdTests --- .../java/org/pgpainless/key/util/UserId.java | 18 +++------------- .../java/org/pgpainless/key/UserIdTest.java | 21 ------------------- 2 files changed, 3 insertions(+), 36 deletions(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/util/UserId.java b/pgpainless-core/src/main/java/org/pgpainless/key/util/UserId.java index 3ac3d77a..71ee8121 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/key/util/UserId.java +++ b/pgpainless-core/src/main/java/org/pgpainless/key/util/UserId.java @@ -22,20 +22,17 @@ public final class UserId implements CharSequence { this.email = email; } - public Builder withName(String name) { - checkNotNull("name", name); + public Builder withName(@Nonnull String name) { this.name = name; return this; } - public Builder withComment(String comment) { - checkNotNull("comment", comment); + public Builder withComment(@Nonnull String comment) { this.comment = comment; return this; } - public Builder withEmail(String email) { - checkNotNull("email", email); + public Builder withEmail(@Nonnull String email) { this.email = email; return this; } @@ -72,13 +69,10 @@ public final class UserId implements CharSequence { } public static UserId onlyEmail(@Nonnull String email) { - checkNotNull("email", email); return new UserId(null, null, email); } public static UserId nameAndEmail(@Nonnull String name, @Nonnull String email) { - checkNotNull("name", name); - checkNotNull("email", email); return new UserId(name, null, email); } @@ -180,10 +174,4 @@ public final class UserId implements CharSequence { || (!valueIsNull && !otherValueIsNull && (ignoreCase ? value.equalsIgnoreCase(otherValue) : value.equals(otherValue))); } - - private static void checkNotNull(String paramName, String value) { - if (value == null) { - throw new IllegalArgumentException(paramName + " must be not null"); - } - } } diff --git a/pgpainless-core/src/test/java/org/pgpainless/key/UserIdTest.java b/pgpainless-core/src/test/java/org/pgpainless/key/UserIdTest.java index 32d56eff..5fe4d9c9 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/key/UserIdTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/key/UserIdTest.java @@ -10,25 +10,9 @@ import org.pgpainless.key.util.UserId; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotEquals; import static org.junit.jupiter.api.Assertions.assertNull; -import static org.junit.jupiter.api.Assertions.assertThrows; public class UserIdTest { - @Test - public void throwForNullName() { - assertThrows(IllegalArgumentException.class, () -> UserId.newBuilder().withName(null)); - } - - @Test - public void throwForNullComment() { - assertThrows(IllegalArgumentException.class, () -> UserId.newBuilder().withComment(null)); - } - - @Test - public void throwForNullEmail() { - assertThrows(IllegalArgumentException.class, () -> UserId.newBuilder().withEmail(null)); - } - @Test public void testFormatOnlyName() { assertEquals( @@ -66,11 +50,6 @@ public class UserIdTest { .toString()); } - @Test - public void throwIfOnlyEmailEmailNull() { - assertThrows(IllegalArgumentException.class, () -> UserId.onlyEmail(null)); - } - @Test public void testNameAndEmail() { UserId userId = UserId.nameAndEmail("Maurice Moss", "moss.m@reynholm.co.uk"); From e69c4a8cf709ee55d5f8e6e0ba65bc9526f15308 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 30 Nov 2022 16:06:01 +0100 Subject: [PATCH 174/763] More UserId tests --- .../java/org/pgpainless/key/UserIdTest.java | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/pgpainless-core/src/test/java/org/pgpainless/key/UserIdTest.java b/pgpainless-core/src/test/java/org/pgpainless/key/UserIdTest.java index 5fe4d9c9..5a553513 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/key/UserIdTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/key/UserIdTest.java @@ -181,4 +181,35 @@ public class UserIdTest { final UserId userId2 = UserId.newBuilder().withComment(comment2).withName(name).withEmail(email).build(); assertNotEquals(userId1, userId2); } + + @Test + public void testLength() { + UserId id = UserId.nameAndEmail("Alice", "alice@pgpainless.org"); + assertEquals(28, id.length()); + } + + @Test + public void testSubSequence() { + UserId id = UserId.onlyEmail("alice@pgpainless.org"); + assertEquals("alice@pgpainless.org", id.subSequence(1, id.length() - 1)); + } + + @Test + public void asStringTest() { + UserId id = UserId.newBuilder() + .withName("Alice") + .withComment("Work Email") + .withEmail("alice@pgpainless.org") + .build(); + + // noinspection deprecation + assertEquals(id.toString(), id.asString()); + } + + @Test + public void charAtTest() { + UserId id = UserId.onlyEmail("alice@pgpainless.org"); + assertEquals('<', id.charAt(0)); + assertEquals('>', id.charAt(id.length() - 1)); + } } From b07e0c2be5952e06c41f79b3271e36b3304c56ba Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 1 Dec 2022 14:48:33 +0100 Subject: [PATCH 175/763] Programmatically confirm that we do not yet support OpenPGP V5 keys :/ --- .../org/bouncycastle/V5OpenPgpKeyTest.java | 61 +++++++++++++++++++ 1 file changed, 61 insertions(+) create mode 100644 pgpainless-core/src/test/java/org/bouncycastle/V5OpenPgpKeyTest.java diff --git a/pgpainless-core/src/test/java/org/bouncycastle/V5OpenPgpKeyTest.java b/pgpainless-core/src/test/java/org/bouncycastle/V5OpenPgpKeyTest.java new file mode 100644 index 00000000..b1337bcb --- /dev/null +++ b/pgpainless-core/src/test/java/org/bouncycastle/V5OpenPgpKeyTest.java @@ -0,0 +1,61 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.bouncycastle; + +import org.bouncycastle.openpgp.PGPPublicKeyRing; +import org.bouncycastle.openpgp.PGPSecretKeyRing; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.pgpainless.PGPainless; + +import java.io.IOException; + +public class V5OpenPgpKeyTest { + + // Both key and cert are provided by Daniel on + // https://mailarchive.ietf.org/arch/msg/openpgp/Z2Mkq9TfvgY5jUJzlNRwgDsDSUk/ + private static final String KEY = "-----BEGIN PGP PRIVATE KEY BLOCK-----\n" + + "\n" + + "xVwFY4d/4xYAAAAtCSsGAQQB2kcPAQEHQPlNp7tI1gph5WdwamWH0DMZmbud\n" + + "iRoIJC6thFQ9+JWjAAD9GXKBexK+cH6NX1hs5hNhIB00TrJmosgv3mg1ditl\n" + + "sLcOpMKkBR8WCgAAAB8FAmOHf+MDCwkHBRUKDggMAhYAAhsDAh4JBScJAgcC\n" + + "AAAAIyIhBRe8+DZtlDb3rzfq2hVsZJRlblqXac8tXLNF+Lg0NvZSecUms7MC\n" + + "rI0Ofp1iKV6QwGFEAQDnd37qxR3r/ezwXEfWUd64NKsHy88o3UG3QasrgR9e\n" + + "SwEAmCPJHs0LvoU81IFsYhEYaZok9uC0DhdnO2lwYUbCTAXHYQVjh3/jEgAA\n" + + "ADIKKwYBBAGXVQEFAQEHQPz3/CmqzgFI9D6tvzoPlpHQoyKiQ2JWJ4Dtkl2o\n" + + "TnFbAwEIBwAA/01gCk95TUR3XFeibg/u/tVY6a//1q0NWC1X+yui3O24Eb3C\n" + + "jgUYFgoAAAAJBQJjh3/jAhsMAAAAIyIhBRe8+DZtlDb3rzfq2hVsZJRlblqX\n" + + "ac8tXLNF+Lg0NvZS78S6dZamUg5K+sXfU/N1umwTAP9JjPVrtnHjtvYTazZm\n" + + "dZhAn8aRLUtGG1owtmLGwCSh6wD/bNrWG4nHfVk/aEHGZ4cjaFlapFr5t1QS\n" + + "psL7nEy94gs=\n" + + "=5xrR\n" + + "-----END PGP PRIVATE KEY BLOCK-----"; + private static final String CERT = "-----BEGIN PGP PUBLIC KEY BLOCK-----\n" + + "\n" + + "xjcFY4d/4xYAAAAtCSsGAQQB2kcPAQEHQPlNp7tI1gph5WdwamWH0DMZmbud\n" + + "iRoIJC6thFQ9+JWjwqQFHxYKAAAAHwUCY4d/4wMLCQcFFQoOCAwCFgACGwMC\n" + + "HgkFJwkCBwIAAAAjIiEFF7z4Nm2UNvevN+raFWxklGVuWpdpzy1cs0X4uDQ2\n" + + "9lJ5xSazswKsjQ5+nWIpXpDAYUQBAOd3furFHev97PBcR9ZR3rg0qwfLzyjd\n" + + "QbdBqyuBH15LAQCYI8kezQu+hTzUgWxiERhpmiT24LQOF2c7aXBhRsJMBc48\n" + + "BWOHf+MSAAAAMgorBgEEAZdVAQUBAQdA/Pf8KarOAUj0Pq2/Og+WkdCjIqJD\n" + + "YlYngO2SXahOcVsDAQgHwo4FGBYKAAAACQUCY4d/4wIbDAAAACMiIQUXvPg2\n" + + "bZQ296836toVbGSUZW5al2nPLVyzRfi4NDb2Uu/EunWWplIOSvrF31Pzdbps\n" + + "EwD/SYz1a7Zx47b2E2s2ZnWYQJ/GkS1LRhtaMLZixsAkoesA/2za1huJx31Z\n" + + "P2hBxmeHI2hZWqRa+bdUEqbC+5xMveIL\n" + + "=sVUI\n" + + "-----END PGP PUBLIC KEY BLOCK-----\n"; + + @Test + @Disabled("BC 1.72 does not yet support V5 keys") + public void testParseCert() throws IOException { + PGPPublicKeyRing cert = PGPainless.readKeyRing().publicKeyRing(CERT); + } + + @Test + @Disabled("BC 1.72 does not yet support V5 keys") + public void testParseKey() throws IOException { + PGPSecretKeyRing key = PGPainless.readKeyRing().secretKeyRing(KEY); + } +} From bfcfaa04c4c0d6ee98ee31d19a1033162fd484e1 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 1 Dec 2022 16:02:45 +0100 Subject: [PATCH 176/763] Add UserId.compare(uid1, uid2, comparator) along with some default comparators --- .../java/org/pgpainless/key/util/UserId.java | 85 +++++++++++++++++++ .../java/org/pgpainless/key/UserIdTest.java | 65 +++++++++++++- 2 files changed, 147 insertions(+), 3 deletions(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/util/UserId.java b/pgpainless-core/src/main/java/org/pgpainless/key/util/UserId.java index 71ee8121..a1b251dc 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/key/util/UserId.java +++ b/pgpainless-core/src/main/java/org/pgpainless/key/util/UserId.java @@ -4,6 +4,7 @@ package org.pgpainless.key.util; +import java.util.Comparator; import javax.annotation.Nonnull; import javax.annotation.Nullable; @@ -174,4 +175,88 @@ public final class UserId implements CharSequence { || (!valueIsNull && !otherValueIsNull && (ignoreCase ? value.equalsIgnoreCase(otherValue) : value.equals(otherValue))); } + + public static int compare(@Nullable UserId o1, @Nullable UserId o2, @Nonnull Comparator comparator) { + return comparator.compare(o1, o2); + } + + public static class DefaultComparator implements Comparator { + + @Override + public int compare(UserId o1, UserId o2) { + if (o1 == o2) { + return 0; + } + if (o1 == null) { + return -1; + } + if (o2 == null) { + return 1; + } + + NullSafeStringComparator c = new NullSafeStringComparator(); + int cName = c.compare(o1.getName(), o2.getName()); + if (cName != 0) { + return cName; + } + + int cComment = c.compare(o1.getComment(), o2.getComment()); + if (cComment != 0) { + return cComment; + } + + return c.compare(o1.getEmail(), o2.getEmail()); + } + } + + public static class DefaultIgnoreCaseComparator implements Comparator { + + @Override + public int compare(UserId o1, UserId o2) { + if (o1 == o2) { + return 0; + } + if (o1 == null) { + return -1; + } + if (o2 == null) { + return 1; + } + + NullSafeStringComparator c = new NullSafeStringComparator(); + int cName = c.compare(lower(o1.getName()), lower(o2.getName())); + if (cName != 0) { + return cName; + } + + int cComment = c.compare(lower(o1.getComment()), lower(o2.getComment())); + if (cComment != 0) { + return cComment; + } + + return c.compare(lower(o1.getEmail()), lower(o2.getEmail())); + } + + private static String lower(String string) { + return string == null ? null : string.toLowerCase(); + } + } + + private static class NullSafeStringComparator implements Comparator { + + @Override + public int compare(String o1, String o2) { + // noinspection StringEquality + if (o1 == o2) { + return 0; + } + if (o1 == null) { + return -1; + } + if (o2 == null) { + return 1; + } + return o1.compareTo(o2); + } + } } diff --git a/pgpainless-core/src/test/java/org/pgpainless/key/UserIdTest.java b/pgpainless-core/src/test/java/org/pgpainless/key/UserIdTest.java index 5a553513..acc90918 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/key/UserIdTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/key/UserIdTest.java @@ -4,13 +4,15 @@ package org.pgpainless.key; -import org.junit.jupiter.api.Test; -import org.pgpainless.key.util.UserId; - import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotEquals; import static org.junit.jupiter.api.Assertions.assertNull; +import java.util.Comparator; + +import org.junit.jupiter.api.Test; +import org.pgpainless.key.util.UserId; + public class UserIdTest { @Test @@ -212,4 +214,61 @@ public class UserIdTest { assertEquals('<', id.charAt(0)); assertEquals('>', id.charAt(id.length() - 1)); } + + @Test + public void defaultCompareTest() { + UserId id1 = UserId.onlyEmail("alice@pgpainless.org"); + UserId id2 = UserId.onlyEmail("alice@gnupg.org"); + UserId id3 = UserId.nameAndEmail("Alice", "alice@pgpainless.org"); + UserId id3_ = UserId.nameAndEmail("Alice", "alice@pgpainless.org"); + UserId id4 = UserId.newBuilder().withName("Alice").build(); + UserId id5 = UserId.newBuilder().withName("Alice").withComment("Work Mail").withEmail("alice@pgpainless.org").build(); + + assertEquals(id3.hashCode(), id3_.hashCode()); + assertNotEquals(id2.hashCode(), id3.hashCode()); + + Comparator c = new UserId.DefaultComparator(); + assertEquals(0, UserId.compare(null, null, c)); + assertEquals(0, UserId.compare(id1, id1, c)); + assertNotEquals(0, UserId.compare(id1, null, c)); + assertNotEquals(0, UserId.compare(null, id1, c)); + assertNotEquals(0, UserId.compare(id1, id2, c)); + assertNotEquals(0, UserId.compare(id2, id1, c)); + assertNotEquals(0, UserId.compare(id1, id3, c)); + assertNotEquals(0, UserId.compare(id1, id4, c)); + assertNotEquals(0, UserId.compare(id4, id1, c)); + assertNotEquals(0, UserId.compare(id2, id3, c)); + assertNotEquals(0, UserId.compare(id1, id5, c)); + assertNotEquals(0, UserId.compare(id5, id1, c)); + assertNotEquals(0, UserId.compare(id3, id5, c)); + assertNotEquals(0, UserId.compare(id5, id3, c)); + assertEquals(0, UserId.compare(id3, id3, c)); + assertEquals(0, UserId.compare(id3, id3_, c)); + } + + @Test + public void defaultIgnoreCaseCompareTest() { + UserId id1 = UserId.nameAndEmail("Alice", "alice@pgpainless.org"); + UserId id2 = UserId.nameAndEmail("alice", "alice@pgpainless.org"); + UserId id3 = UserId.nameAndEmail("Alice", "Alice@Pgpainless.Org"); + UserId id4 = UserId.newBuilder().withName("Alice").withComment("Work Email").withEmail("Alice@Pgpainless.Org").build(); + UserId id5 = UserId.newBuilder().withName("alice").withComment("work email").withEmail("alice@pgpainless.org").build(); + UserId id6 = UserId.nameAndEmail("Bob", "bob@pgpainless.org"); + + Comparator c = new UserId.DefaultIgnoreCaseComparator(); + assertEquals(0, UserId.compare(id1, id2, c)); + assertEquals(0, UserId.compare(id1, id3, c)); + assertEquals(0, UserId.compare(id2, id3, c)); + assertEquals(0, UserId.compare(null, null, c)); + assertEquals(0, UserId.compare(id1, id1, c)); + assertEquals(0, UserId.compare(id4, id4, c)); + assertEquals(0, UserId.compare(id4, id5, c)); + assertEquals(0, UserId.compare(id5, id4, c)); + assertNotEquals(0, UserId.compare(null, id1, c)); + assertNotEquals(0, UserId.compare(id1, null, c)); + assertNotEquals(0, UserId.compare(id1, id4, c)); + assertNotEquals(0, UserId.compare(id4, id1, c)); + assertNotEquals(0, UserId.compare(id1, id6, c)); + assertNotEquals(0, UserId.compare(id6, id1, c)); + } } From 907d1c4d1c2eda73ee662f35ea19cf5586d6c301 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 1 Dec 2022 16:04:45 +0100 Subject: [PATCH 177/763] move V5OpenPgpKeyTest to org.pgpainless.key --- .../org/{bouncycastle => pgpainless/key}/V5OpenPgpKeyTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename pgpainless-core/src/test/java/org/{bouncycastle => pgpainless/key}/V5OpenPgpKeyTest.java (99%) diff --git a/pgpainless-core/src/test/java/org/bouncycastle/V5OpenPgpKeyTest.java b/pgpainless-core/src/test/java/org/pgpainless/key/V5OpenPgpKeyTest.java similarity index 99% rename from pgpainless-core/src/test/java/org/bouncycastle/V5OpenPgpKeyTest.java rename to pgpainless-core/src/test/java/org/pgpainless/key/V5OpenPgpKeyTest.java index b1337bcb..d1b941fd 100644 --- a/pgpainless-core/src/test/java/org/bouncycastle/V5OpenPgpKeyTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/key/V5OpenPgpKeyTest.java @@ -2,7 +2,7 @@ // // SPDX-License-Identifier: Apache-2.0 -package org.bouncycastle; +package org.pgpainless.key; import org.bouncycastle.openpgp.PGPPublicKeyRing; import org.bouncycastle.openpgp.PGPSecretKeyRing; From 218da50da30f6751e0b498fcfcc433b1a2643adb Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 5 Dec 2022 13:47:54 +0100 Subject: [PATCH 178/763] Create gradle mavenCentralChecksums task to quickly fetch checksums of published artifacts gradle mavenCentralChecksums will fetch the checksums of the currently checked out release, while gradle -Prelease=1.3.13 for example will fetch those of the 1.3.13 release --- build.gradle | 41 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/build.gradle b/build.gradle index 7802bdb1..6e086754 100644 --- a/build.gradle +++ b/build.gradle @@ -257,3 +257,44 @@ task javadocAll(type: Javadoc) { "https://docs.oracle.com/javase/${sourceCompatibility.getMajorVersion()}/docs/api/", ] as String[] } + +/** + * Fetch sha256 checksums of artifacts published to maven central. + * + * Example: gradle -Prelease=1.3.13 mavenCentralChecksums + */ +task mavenCentralChecksums() { + description 'Fetch and display checksums for artifacts published to Maven Central' + String ver = project.hasProperty('release') ? release : shortVersion + doLast { + Process p = "curl -f https://repo1.maven.org/maven2/org/pgpainless/pgpainless-core/${ver}/pgpainless-core-${ver}.jar.sha256".execute() + if (p.waitFor() == 0) { + print p.text.trim() + println " pgpainless-core/build/libs/pgpainless-core-${ver}.jar" + } + + p = "curl -f https://repo1.maven.org/maven2/org/pgpainless/pgpainless-sop/${ver}/pgpainless-sop-${ver}.jar.sha256".execute() + if (p.waitFor() == 0) { + print p.text.trim() + println " pgpainless-sop/build/libs/pgpainless-sop-${ver}.jar" + } + + p = "curl -f https://repo1.maven.org/maven2/org/pgpainless/pgpainless-cli/${ver}/pgpainless-cli-${ver}-all.jar.sha256".execute() + if (p.waitFor() == 0) { + print p.text.trim() + println " pgpainless-cli/build/libs/pgpainless-cli-${ver}-all.jar" + } + + p = "curl -f https://repo1.maven.org/maven2/org/pgpainless/pgpainless-cli/${ver}/pgpainless-cli-${ver}.jar.sha256".execute() + if (p.waitFor() == 0) { + print p.text.trim() + println " pgpainless-cli/build/libs/pgpainless-cli-${ver}.jar" + } + + p = "curl -f https://repo1.maven.org/maven2/org/pgpainless/hsregex/${ver}/hsregex-${ver}.jar.sha256".execute() + if (p.waitFor() == 0) { + print p.text.trim() + println " hsregex/build/libs/hsregex-${ver}.jar" + } + } +} From e168ac6f557f96be478d7d979575dfeb3892c700 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 5 Dec 2022 14:00:34 +0100 Subject: [PATCH 179/763] Update documentation to use new MessageMetadata class --- docs/source/pgpainless-core/quickstart.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/docs/source/pgpainless-core/quickstart.md b/docs/source/pgpainless-core/quickstart.md index 371a5f6f..d61a881c 100644 --- a/docs/source/pgpainless-core/quickstart.md +++ b/docs/source/pgpainless-core/quickstart.md @@ -242,7 +242,7 @@ or the passphrase. ### Decrypt and/or Verify a Message Decryption and verification of a message is both done using the same API. Whether a message was actually signed / encrypted can be determined after the message has been processed by checking -the `OpenPgpMetadata` object which can be obtained from the `DecryptionStream`. +the `MessageMetadata` object which can be obtained from the `DecryptionStream`. To configure the decryption / verification process, the `ConsumerOptions` object is used: @@ -283,16 +283,16 @@ Streams.pipeAll(consumerStream, plaintext); consumerStream.close(); // important! // The result will contain metadata of the message -OpenPgpMetadata result = consumerStream.getResult(); +MessageMetadata result = consumerStream.getMetadata(); ``` -After the message has been processed, you can consult the `OpenPgpMetadata` object to determine the nature of the message: +After the message has been processed, you can consult the `MessageMetadata` object to determine the nature of the message: ```java boolean wasEncrypted = result.isEncrypted(); SubkeyIdentifier decryptionKey = result.getDecryptionKey(); -Map validSignatures = result.getVerifiedSignatures(); -boolean wasSignedByCert = result.containsVerifiedSignatureFrom(certificate); +List validSignatures = result.getVerifiedSignatures(); +boolean wasSignedByCert = result.isVerifiedSignedBy(certificate); // For files: String fileName = result.getFileName(); @@ -323,6 +323,6 @@ DecryptionStream verificationStream = PGPainless.decryptAndOrVerify() Streams.drain(verificationStream); // push all the data through the stream verificationStream.close(); // finish verification -OpenPgpMetadata result = verificationStream.getResult(); // get metadata of signed message -assertTrue(result.containsVerifiedSignatureFrom(certificate)); // check if message was in fact signed +MessageMetadata result = verificationStream.getMetadata(); // get metadata of signed message +assertTrue(result.isVerifiedSignedBy(certificate)); // check if message was in fact signed ``` \ No newline at end of file From f5414bcc196c91da22d40be4dc32e10ead488c08 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 13 Dec 2022 16:11:17 +0100 Subject: [PATCH 180/763] Use proper method to unlock private key when detached-signing --- .../java/org/pgpainless/encryption_signing/SigningOptions.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/SigningOptions.java b/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/SigningOptions.java index 0a7c47c4..0af07fc9 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/SigningOptions.java +++ b/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/SigningOptions.java @@ -359,8 +359,7 @@ public final class SigningOptions { if (signingSecKey == null) { throw new KeyException.MissingSecretKeyException(OpenPgpFingerprint.of(secretKey), signingPubKey.getKeyID()); } - PGPPrivateKey signingSubkey = signingSecKey.extractPrivateKey( - secretKeyDecryptor.getDecryptor(signingPubKey.getKeyID())); + PGPPrivateKey signingSubkey = UnlockSecretKey.unlockSecretKey(signingSecKey, secretKeyDecryptor); Set hashAlgorithms = userId != null ? keyRingInfo.getPreferredHashAlgorithms(userId) : keyRingInfo.getPreferredHashAlgorithms(signingPubKey.getKeyID()); HashAlgorithm hashAlgorithm = negotiateHashAlgorithm(hashAlgorithms, PGPainless.getPolicy()); From 4f435a0fa0dd9d70383d1c0c9e8eaf832a5a102e Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 13 Dec 2022 16:15:32 +0100 Subject: [PATCH 181/763] Fix parameter check for DSA keys Fixes #345 --- .../PublicKeyParameterValidationUtil.java | 2 +- .../sop/CarolKeySignEncryptRoundtripTest.java | 295 ++++++++++++++++++ 2 files changed, 296 insertions(+), 1 deletion(-) create mode 100644 pgpainless-sop/src/test/java/org/pgpainless/sop/CarolKeySignEncryptRoundtripTest.java diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/util/PublicKeyParameterValidationUtil.java b/pgpainless-core/src/main/java/org/pgpainless/key/util/PublicKeyParameterValidationUtil.java index 69940691..88e9897a 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/key/util/PublicKeyParameterValidationUtil.java +++ b/pgpainless-core/src/main/java/org/pgpainless/key/util/PublicKeyParameterValidationUtil.java @@ -195,7 +195,7 @@ public class PublicKeyParameterValidationUtil { } // q > 160 bits - boolean qLarge = pQ.getLowestSetBit() > 160; + boolean qLarge = pQ.bitLength() > 160; if (!qLarge) { return false; } diff --git a/pgpainless-sop/src/test/java/org/pgpainless/sop/CarolKeySignEncryptRoundtripTest.java b/pgpainless-sop/src/test/java/org/pgpainless/sop/CarolKeySignEncryptRoundtripTest.java new file mode 100644 index 00000000..81bf7645 --- /dev/null +++ b/pgpainless-sop/src/test/java/org/pgpainless/sop/CarolKeySignEncryptRoundtripTest.java @@ -0,0 +1,295 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.sop; + +import java.io.IOException; + +import org.junit.jupiter.api.Test; +import sop.ByteArrayAndResult; +import sop.DecryptionResult; +import sop.Ready; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class CarolKeySignEncryptRoundtripTest { + + private static final String CAROL_KEY = "-----BEGIN PGP PRIVATE KEY BLOCK-----\n" + + "\n" + + "xcQTBF3+CmgRDADZhdKTM3ms3XpXnQke83FgaIBtP1g1qhqpCfg50WiPS0kjiMC0\n" + + "OJz2vh59nusbBLzgI//Y1VMhKfIWYbqMcIY+lWbseHjl52rqW6AaJ0TH4NgVt7vh\n" + + "yVeJt0k/NnxvNhMd0587KXmfpDxrwBqc/l5cVB+p0rL8vs8kxojHXAi5V3koM0Uj\n" + + "REWs5Jpj/XU9LhEoyXZkeJC/pes1u6UKoFYn7dFIP49Kkd1kb+1bNfdPYtA0JpcG\n" + + "zYgeMNOvdWJwn43dNhxoeuXfmAEhA8LdzT0C0O+7akXOKWrfhXJ8MTBqvPgWZYx7\n" + + "MNuQx/ejIMZHl+Iaf7hG976ILH+NCGiKkhidd9GIuA/WteHiQbXLyfiQ4n8P12q9\n" + + "+4dq6ybUM65tnozRyyN+1m3rU2a/+Ly3JCh4TeO27w+cxMWkaeHyTQaJVMbMbDpX\n" + + "duVd32MA33UVNH5/KXMVczVi5asVjuKDSojJDV1QwX8izZNl1t+AI0L3balCabV0\n" + + "SFhlfnBEUj1my1sBAMOSO/I67BvBS3IPHZWXHjgclhs26mPzRlZLryAUWR2DDACH\n" + + "5fx+yUAdZ8Vu/2zWTHxwWJ/X6gGTLqa9CmfDq5UDqYFFzuWwN4HJ+ryOuak1CGwS\n" + + "KJUBSA75HExbv0naWg+suy+pEDvF0VALPU9VUkSQtHyR10YO2FWOe3AEtpbYDRwp\n" + + "dr1ZwEbb3L6IGQ5i/4CNHbJ2u3yUeXsDNAvrpVSEcIjA01RPCOKmf58SDZp4yDdP\n" + + "xGhM8w6a18+fdQr22f2cJ0xgfPlbzFbO+FUsEgKvn6QTLhbaYw4zs7rdQDejWHV8\n" + + "2hP4K+rb9FwknYdV9uo4m77MgGlU+4yvJnGEYaL3jwjI3bH9aooNOl6XbvVAzNzo\n" + + "mYmaTO7mp6xFAu43yuGyd9K+1E4k7CQTROxTZ+RdtQjV95hSsEmMg792nQvDSBW4\n" + + "xwfOQ7pf3kC7r9fm8u9nBlEN12HsbQ8Yvux/ld5q5RaIlD19jzfVR6+hJzbj2ZnU\n" + + "yQs4ksAfIHTzTdLttRxS9lTRTkVx2vbUnoSBy6TYF1mf6nRPpSm1riZxnkR4+BQL\n" + + "/0rUAxwegTNIG/5M612s2a45QvYK1turZ7spI1RGitJUIjBXUuR76jIsyqagIhBl\n" + + "5nEsQ4HLv8OQ3EgJ5T9gldLFpHNczLxBQnnNwfPoD2e0kC/iy0rfiNX8HWpTgQpb\n" + + "zAosLj5/E0iNlildynIhuqBosyRWFqGva0O6qioL90srlzlfKCloe9R9w3HizjCb\n" + + "f59yEspuJt9iHVNOPOW2Wj5ub0KTiJPp9vBmrFaB79/IlgojpQoYvQ77Hx5A9CJq\n" + + "paMCHGOW6Uz9euN1ozzETEkIPtL8XAxcogfpe2JKE1uS7ugxsKEGEDfxOQFKAGV0\n" + + "XFtIx50vFCr2vQro0WB858CGN47dCxChhNUxNtGc11JNEkNv/X7hKtRf/5VCmnaz\n" + + "GWwNK47cqZ7GJfEBnElD7s/tQvTC5Qp7lg9gEt47TUX0bjzUTCxNvLosuKL9+J1W\n" + + "ln1myRpff/5ZOAnZTPHR+AbX4bRB4sK5zijQe4139Dn2oRYK+EIYoBAxFxSOzehP\n" + + "IQAA/2BCN5HryGjVff2t7Q6fVrQQS9hsMisszZl5rWwUOO6zETHCigQfEQgAPAUC\n" + + "Xf4KaQMLCQoJEJunidx21oSaBBUKCQgCFgECF4ACGwMCHgEWIQRx/9oARAnl3bDD\n" + + "6PGbp4ncdtaEmgAAYoUA/1VpxdR2wYT/pC8FrKsbmIxLJRLDNlED3ihivWp/B2e/\n" + + "AQCT2oi9zqbjprCKAnzoIYTGTil4yFfmeey8GjMOxUHz4M0mQ2Fyb2wgT2xkc3R5\n" + + "bGUgPGNhcm9sQG9wZW5wZ3AuZXhhbXBsZT7CigQTEQgAPAUCXf4KaQMLCQoJEJun\n" + + "idx21oSaBBUKCQgCFgECF4ACGwMCHgEWIQRx/9oARAnl3bDD6PGbp4ncdtaEmgAA\n" + + "UEwA/2TFwL0mymjCSaQH8KdQuygI+itpNggM+Y8FF8hn9fo1AP9ogDIl9V3C8t59\n" + + "C/Mrc4HvP1ABR2nwZeK5+A5lLoH4Y8fD8QRd/gpoEAwA2YXSkzN5rN16V50JHvNx\n" + + "YGiAbT9YNaoaqQn4OdFoj0tJI4jAtDic9r4efZ7rGwS84CP/2NVTISnyFmG6jHCG\n" + + "PpVm7Hh45edq6lugGidEx+DYFbe74clXibdJPzZ8bzYTHdOfOyl5n6Q8a8AanP5e\n" + + "XFQfqdKy/L7PJMaIx1wIuVd5KDNFI0RFrOSaY/11PS4RKMl2ZHiQv6XrNbulCqBW\n" + + "J+3RSD+PSpHdZG/tWzX3T2LQNCaXBs2IHjDTr3VicJ+N3TYcaHrl35gBIQPC3c09\n" + + "AtDvu2pFzilq34VyfDEwarz4FmWMezDbkMf3oyDGR5fiGn+4Rve+iCx/jQhoipIY\n" + + "nXfRiLgP1rXh4kG1y8n4kOJ/D9dqvfuHausm1DOubZ6M0csjftZt61Nmv/i8tyQo\n" + + "eE3jtu8PnMTFpGnh8k0GiVTGzGw6V3blXd9jAN91FTR+fylzFXM1YuWrFY7ig0qI\n" + + "yQ1dUMF/Is2TZdbfgCNC922pQmm1dEhYZX5wRFI9ZstbDACH5fx+yUAdZ8Vu/2zW\n" + + "THxwWJ/X6gGTLqa9CmfDq5UDqYFFzuWwN4HJ+ryOuak1CGwSKJUBSA75HExbv0na\n" + + "Wg+suy+pEDvF0VALPU9VUkSQtHyR10YO2FWOe3AEtpbYDRwpdr1ZwEbb3L6IGQ5i\n" + + "/4CNHbJ2u3yUeXsDNAvrpVSEcIjA01RPCOKmf58SDZp4yDdPxGhM8w6a18+fdQr2\n" + + "2f2cJ0xgfPlbzFbO+FUsEgKvn6QTLhbaYw4zs7rdQDejWHV82hP4K+rb9FwknYdV\n" + + "9uo4m77MgGlU+4yvJnGEYaL3jwjI3bH9aooNOl6XbvVAzNzomYmaTO7mp6xFAu43\n" + + "yuGyd9K+1E4k7CQTROxTZ+RdtQjV95hSsEmMg792nQvDSBW4xwfOQ7pf3kC7r9fm\n" + + "8u9nBlEN12HsbQ8Yvux/ld5q5RaIlD19jzfVR6+hJzbj2ZnUyQs4ksAfIHTzTdLt\n" + + "tRxS9lTRTkVx2vbUnoSBy6TYF1mf6nRPpSm1riZxnkR4+BQL/jEGmn1tLhxfjfDA\n" + + "5vFFj73+FXdFCdFKSI0VpdoU1fgR5DX72ZQUYYUCKYTYikXv1mqdH/5VthptrktC\n" + + "oAco4zVxM04sK7Xthl+uTOhei8/Dd9ZLdSIoNcRjrr/uh5sUzUfIC9iuT3SXiZ/D\n" + + "0yVq0Uu/gWPB3ZIG/sFacxOXAr6RYhvz9MqnwXS1sVT5TyO3XIQ5JseIgIRyV/Sf\n" + + "4F/4Qui9wMzzSajTwCsttMGKf67k228AaJVv+IpFoo+OtCa7wbJukqfNQN3m2ojf\n" + + "V5CcoCzsoRsoTInhrpQmM+gGoQBXBArT1xk3KK3VdZibYfMoxeIGXw0MoNJzFuGK\n" + + "+PcnhV3ETFMNcszd0Pb9s86g7hYtpRmE12Jlai2MzPSmyztlsRP9tcZwYy7JdPZf\n" + + "xXQP24XWat7eP2qWxTnkEP4/wKYb81m7CZ4RvUO/nd1aA5c9IBYknbgmCAAKvHVD\n" + + "iTY61E5GbC9aTiI4WIwjItroikukUJE+p77rpjxfw/1U51BnmQAA/ih5jIthn2ZE\n" + + "r1YoOsUs8CBhylTsRZK6VS4ZCErcyl2tD2LCigQYEQgAPAUCXf4KaQMLCQoJEJun\n" + + "idx21oSaBBUKCQgCFgECF4ACGwwCHgEWIQRx/9oARAnl3bDD6PGbp4ncdtaEmgAA\n" + + "QSkA/3WEWqZxvZmpVxpEMxJWaGQRwUhGake8OhC1WfywCtarAQCLwfBsyEv5jBEi\n" + + "1FkOSekLi8WNMdUx3XMyvP8nJ65P2Q==\n" + + "=Xj8h\n" + + "-----END PGP PRIVATE KEY BLOCK-----\n"; + + private static final String CAROL_CERT = "-----BEGIN PGP PUBLIC KEY BLOCK-----\n" + + "\n" + + "xsPuBF3+CmgRDADZhdKTM3ms3XpXnQke83FgaIBtP1g1qhqpCfg50WiPS0kjiMC0\n" + + "OJz2vh59nusbBLzgI//Y1VMhKfIWYbqMcIY+lWbseHjl52rqW6AaJ0TH4NgVt7vh\n" + + "yVeJt0k/NnxvNhMd0587KXmfpDxrwBqc/l5cVB+p0rL8vs8kxojHXAi5V3koM0Uj\n" + + "REWs5Jpj/XU9LhEoyXZkeJC/pes1u6UKoFYn7dFIP49Kkd1kb+1bNfdPYtA0JpcG\n" + + "zYgeMNOvdWJwn43dNhxoeuXfmAEhA8LdzT0C0O+7akXOKWrfhXJ8MTBqvPgWZYx7\n" + + "MNuQx/ejIMZHl+Iaf7hG976ILH+NCGiKkhidd9GIuA/WteHiQbXLyfiQ4n8P12q9\n" + + "+4dq6ybUM65tnozRyyN+1m3rU2a/+Ly3JCh4TeO27w+cxMWkaeHyTQaJVMbMbDpX\n" + + "duVd32MA33UVNH5/KXMVczVi5asVjuKDSojJDV1QwX8izZNl1t+AI0L3balCabV0\n" + + "SFhlfnBEUj1my1sBAMOSO/I67BvBS3IPHZWXHjgclhs26mPzRlZLryAUWR2DDACH\n" + + "5fx+yUAdZ8Vu/2zWTHxwWJ/X6gGTLqa9CmfDq5UDqYFFzuWwN4HJ+ryOuak1CGwS\n" + + "KJUBSA75HExbv0naWg+suy+pEDvF0VALPU9VUkSQtHyR10YO2FWOe3AEtpbYDRwp\n" + + "dr1ZwEbb3L6IGQ5i/4CNHbJ2u3yUeXsDNAvrpVSEcIjA01RPCOKmf58SDZp4yDdP\n" + + "xGhM8w6a18+fdQr22f2cJ0xgfPlbzFbO+FUsEgKvn6QTLhbaYw4zs7rdQDejWHV8\n" + + "2hP4K+rb9FwknYdV9uo4m77MgGlU+4yvJnGEYaL3jwjI3bH9aooNOl6XbvVAzNzo\n" + + "mYmaTO7mp6xFAu43yuGyd9K+1E4k7CQTROxTZ+RdtQjV95hSsEmMg792nQvDSBW4\n" + + "xwfOQ7pf3kC7r9fm8u9nBlEN12HsbQ8Yvux/ld5q5RaIlD19jzfVR6+hJzbj2ZnU\n" + + "yQs4ksAfIHTzTdLttRxS9lTRTkVx2vbUnoSBy6TYF1mf6nRPpSm1riZxnkR4+BQL\n" + + "/0rUAxwegTNIG/5M612s2a45QvYK1turZ7spI1RGitJUIjBXUuR76jIsyqagIhBl\n" + + "5nEsQ4HLv8OQ3EgJ5T9gldLFpHNczLxBQnnNwfPoD2e0kC/iy0rfiNX8HWpTgQpb\n" + + "zAosLj5/E0iNlildynIhuqBosyRWFqGva0O6qioL90srlzlfKCloe9R9w3HizjCb\n" + + "f59yEspuJt9iHVNOPOW2Wj5ub0KTiJPp9vBmrFaB79/IlgojpQoYvQ77Hx5A9CJq\n" + + "paMCHGOW6Uz9euN1ozzETEkIPtL8XAxcogfpe2JKE1uS7ugxsKEGEDfxOQFKAGV0\n" + + "XFtIx50vFCr2vQro0WB858CGN47dCxChhNUxNtGc11JNEkNv/X7hKtRf/5VCmnaz\n" + + "GWwNK47cqZ7GJfEBnElD7s/tQvTC5Qp7lg9gEt47TUX0bjzUTCxNvLosuKL9+J1W\n" + + "ln1myRpff/5ZOAnZTPHR+AbX4bRB4sK5zijQe4139Dn2oRYK+EIYoBAxFxSOzehP\n" + + "IcKKBB8RCAA8BQJd/gppAwsJCgkQm6eJ3HbWhJoEFQoJCAIWAQIXgAIbAwIeARYh\n" + + "BHH/2gBECeXdsMPo8Zunidx21oSaAABihQD/VWnF1HbBhP+kLwWsqxuYjEslEsM2\n" + + "UQPeKGK9an8HZ78BAJPaiL3OpuOmsIoCfOghhMZOKXjIV+Z57LwaMw7FQfPgzSZD\n" + + "YXJvbCBPbGRzdHlsZSA8Y2Fyb2xAb3BlbnBncC5leGFtcGxlPsKKBBMRCAA8BQJd\n" + + "/gppAwsJCgkQm6eJ3HbWhJoEFQoJCAIWAQIXgAIbAwIeARYhBHH/2gBECeXdsMPo\n" + + "8Zunidx21oSaAABQTAD/ZMXAvSbKaMJJpAfwp1C7KAj6K2k2CAz5jwUXyGf1+jUA\n" + + "/2iAMiX1XcLy3n0L8ytzge8/UAFHafBl4rn4DmUugfhjzsPMBF3+CmgQDADZhdKT\n" + + "M3ms3XpXnQke83FgaIBtP1g1qhqpCfg50WiPS0kjiMC0OJz2vh59nusbBLzgI//Y\n" + + "1VMhKfIWYbqMcIY+lWbseHjl52rqW6AaJ0TH4NgVt7vhyVeJt0k/NnxvNhMd0587\n" + + "KXmfpDxrwBqc/l5cVB+p0rL8vs8kxojHXAi5V3koM0UjREWs5Jpj/XU9LhEoyXZk\n" + + "eJC/pes1u6UKoFYn7dFIP49Kkd1kb+1bNfdPYtA0JpcGzYgeMNOvdWJwn43dNhxo\n" + + "euXfmAEhA8LdzT0C0O+7akXOKWrfhXJ8MTBqvPgWZYx7MNuQx/ejIMZHl+Iaf7hG\n" + + "976ILH+NCGiKkhidd9GIuA/WteHiQbXLyfiQ4n8P12q9+4dq6ybUM65tnozRyyN+\n" + + "1m3rU2a/+Ly3JCh4TeO27w+cxMWkaeHyTQaJVMbMbDpXduVd32MA33UVNH5/KXMV\n" + + "czVi5asVjuKDSojJDV1QwX8izZNl1t+AI0L3balCabV0SFhlfnBEUj1my1sMAIfl\n" + + "/H7JQB1nxW7/bNZMfHBYn9fqAZMupr0KZ8OrlQOpgUXO5bA3gcn6vI65qTUIbBIo\n" + + "lQFIDvkcTFu/SdpaD6y7L6kQO8XRUAs9T1VSRJC0fJHXRg7YVY57cAS2ltgNHCl2\n" + + "vVnARtvcvogZDmL/gI0dsna7fJR5ewM0C+ulVIRwiMDTVE8I4qZ/nxINmnjIN0/E\n" + + "aEzzDprXz591CvbZ/ZwnTGB8+VvMVs74VSwSAq+fpBMuFtpjDjOzut1AN6NYdXza\n" + + "E/gr6tv0XCSdh1X26jibvsyAaVT7jK8mcYRhovePCMjdsf1qig06Xpdu9UDM3OiZ\n" + + "iZpM7uanrEUC7jfK4bJ30r7UTiTsJBNE7FNn5F21CNX3mFKwSYyDv3adC8NIFbjH\n" + + "B85Dul/eQLuv1+by72cGUQ3XYextDxi+7H+V3mrlFoiUPX2PN9VHr6EnNuPZmdTJ\n" + + "CziSwB8gdPNN0u21HFL2VNFORXHa9tSehIHLpNgXWZ/qdE+lKbWuJnGeRHj4FAv+\n" + + "MQaafW0uHF+N8MDm8UWPvf4Vd0UJ0UpIjRWl2hTV+BHkNfvZlBRhhQIphNiKRe/W\n" + + "ap0f/lW2Gm2uS0KgByjjNXEzTiwrte2GX65M6F6Lz8N31kt1Iig1xGOuv+6HmxTN\n" + + "R8gL2K5PdJeJn8PTJWrRS7+BY8Hdkgb+wVpzE5cCvpFiG/P0yqfBdLWxVPlPI7dc\n" + + "hDkmx4iAhHJX9J/gX/hC6L3AzPNJqNPAKy20wYp/ruTbbwBolW/4ikWij460JrvB\n" + + "sm6Sp81A3ebaiN9XkJygLOyhGyhMieGulCYz6AahAFcECtPXGTcordV1mJth8yjF\n" + + "4gZfDQyg0nMW4Yr49yeFXcRMUw1yzN3Q9v2zzqDuFi2lGYTXYmVqLYzM9KbLO2Wx\n" + + "E/21xnBjLsl09l/FdA/bhdZq3t4/apbFOeQQ/j/AphvzWbsJnhG9Q7+d3VoDlz0g\n" + + "FiSduCYIAAq8dUOJNjrUTkZsL1pOIjhYjCMi2uiKS6RQkT6nvuumPF/D/VTnUGeZ\n" + + "wooEGBEIADwFAl3+CmkDCwkKCRCbp4ncdtaEmgQVCgkIAhYBAheAAhsMAh4BFiEE\n" + + "cf/aAEQJ5d2ww+jxm6eJ3HbWhJoAAEEpAP91hFqmcb2ZqVcaRDMSVmhkEcFIRmpH\n" + + "vDoQtVn8sArWqwEAi8HwbMhL+YwRItRZDknpC4vFjTHVMd1zMrz/JyeuT9k=\n" + + "=pa/S\n" + + "-----END PGP PUBLIC KEY BLOCK-----\n"; + + private static final String BOB_KEY = "-----BEGIN PGP PRIVATE KEY BLOCK-----\n" + + "Comment: Bob's OpenPGP Transferable Secret Key\n" + + "\n" + + "lQVYBF2lnPIBDAC5cL9PQoQLTMuhjbYvb4Ncuuo0bfmgPRFywX53jPhoFf4Zg6mv\n" + + "/seOXpgecTdOcVttfzC8ycIKrt3aQTiwOG/ctaR4Bk/t6ayNFfdUNxHWk4WCKzdz\n" + + "/56fW2O0F23qIRd8UUJp5IIlN4RDdRCtdhVQIAuzvp2oVy/LaS2kxQoKvph/5pQ/\n" + + "5whqsyroEWDJoSV0yOb25B/iwk/pLUFoyhDG9bj0kIzDxrEqW+7Ba8nocQlecMF3\n" + + "X5KMN5kp2zraLv9dlBBpWW43XktjcCZgMy20SouraVma8Je/ECwUWYUiAZxLIlMv\n" + + "9CurEOtxUw6N3RdOtLmYZS9uEnn5y1UkF88o8Nku890uk6BrewFzJyLAx5wRZ4F0\n" + + "qV/yq36UWQ0JB/AUGhHVPdFf6pl6eaxBwT5GXvbBUibtf8YI2og5RsgTWtXfU7eb\n" + + "SGXrl5ZMpbA6mbfhd0R8aPxWfmDWiIOhBufhMCvUHh1sApMKVZnvIff9/0Dca3wb\n" + + "vLIwa3T4CyshfT0AEQEAAQAL/RZqbJW2IqQDCnJi4Ozm++gPqBPiX1RhTWSjwxfM\n" + + "cJKUZfzLj414rMKm6Jh1cwwGY9jekROhB9WmwaaKT8HtcIgrZNAlYzANGRCM4TLK\n" + + "3VskxfSwKKna8l+s+mZglqbAjUg3wmFuf9Tj2xcUZYmyRm1DEmcN2ZzpvRtHgX7z\n" + + "Wn1mAKUlSDJZSQks0zjuMNbupcpyJokdlkUg2+wBznBOTKzgMxVNC9b2g5/tMPUs\n" + + "hGGWmF1UH+7AHMTaS6dlmr2ZBIyogdnfUqdNg5sZwsxSNrbglKP4sqe7X61uEAIQ\n" + + "bD7rT3LonLbhkrj3I8wilUD8usIwt5IecoHhd9HziqZjRCc1BUBkboUEoyedbDV4\n" + + "i4qfsFZ6CEWoLuD5pW7dEp0M+WeuHXO164Rc+LnH6i1VQrpb1Okl4qO6ejIpIjBI\n" + + "1t3GshtUu/mwGBBxs60KBX5g77mFQ9lLCRj8lSYqOsHRKBhUp4qM869VA+fD0BRP\n" + + "fqPT0I9IH4Oa/A3jYJcg622GwQYA1LhnP208Waf6PkQSJ6kyr8ymY1yVh9VBE/g6\n" + + "fRDYA+pkqKnw9wfH2Qho3ysAA+OmVOX8Hldg+Pc0Zs0e5pCavb0En8iFLvTA0Q2E\n" + + "LR5rLue9uD7aFuKFU/VdcddY9Ww/vo4k5p/tVGp7F8RYCFn9rSjIWbfvvZi1q5Tx\n" + + "+akoZbga+4qQ4WYzB/obdX6SCmi6BndcQ1QdjCCQU6gpYx0MddVERbIp9+2SXDyL\n" + + "hpxjSyz+RGsZi/9UAshT4txP4+MZBgDfK3ZqtW+h2/eMRxkANqOJpxSjMyLO/FXN\n" + + "WxzTDYeWtHNYiAlOwlQZEPOydZFty9IVzzNFQCIUCGjQ/nNyhw7adSgUk3+BXEx/\n" + + "MyJPYY0BYuhLxLYcrfQ9nrhaVKxRJj25SVHj2ASsiwGJRZW4CC3uw40OYxfKEvNC\n" + + "mer/VxM3kg8qqGf9KUzJ1dVdAvjyx2Hz6jY2qWCyRQ6IMjWHyd43C4r3jxooYKUC\n" + + "YnstRQyb/gCSKahveSEjo07CiXMr88UGALwzEr3npFAsPW3osGaFLj49y1oRe11E\n" + + "he9gCHFm+fuzbXrWmdPjYU5/ZdqdojzDqfu4ThfnipknpVUM1o6MQqkjM896FHm8\n" + + "zbKVFSMhEP6DPHSCexMFrrSgN03PdwHTO6iBaIBBFqmGY01tmJ03SxvSpiBPON9P\n" + + "NVvy/6UZFedTq8A07OUAxO62YUSNtT5pmK2vzs3SAZJmbFbMh+NN204TRI72GlqT\n" + + "t5hcfkuv8hrmwPS/ZR6q312mKQ6w/1pqO9qitCFCb2IgQmFiYmFnZSA8Ym9iQG9w\n" + + "ZW5wZ3AuZXhhbXBsZT6JAc4EEwEKADgCGwMFCwkIBwIGFQoJCAsCBBYCAwECHgEC\n" + + "F4AWIQTRpm4aI7GCyZgPeIz7/MgqAV5zMAUCXaWe+gAKCRD7/MgqAV5zMG9sC/9U\n" + + "2T3RrqEbw533FPNfEflhEVRIZ8gDXKM8hU6cqqEzCmzZT6xYTe6sv4y+PJBGXJFX\n" + + "yhj0g6FDkSyboM5litOcTupURObVqMgA/Y4UKERznm4fzzH9qek85c4ljtLyNufe\n" + + "doL2pp3vkGtn7eD0QFRaLLmnxPKQ/TlZKdLE1G3u8Uot8QHicaR6GnAdc5UXQJE3\n" + + "BiV7jZuDyWmZ1cUNwJkKL6oRtp+ZNDOQCrLNLecKHcgCqrpjSQG5oouba1I1Q6Vl\n" + + "sP44dhA1nkmLHtxlTOzpeHj4jnk1FaXmyasurrrI5CgU/L2Oi39DGKTH/A/cywDN\n" + + "4ZplIQ9zR8enkbXquUZvFDe+Xz+6xRXtb5MwQyWODB3nHw85HocLwRoIN9WdQEI+\n" + + "L8a/56AuOwhs8llkSuiITjR7r9SgKJC2WlAHl7E8lhJ3VDW3ELC56KH308d6mwOG\n" + + "ZRAqIAKzM1T5FGjMBhq7ZV0eqdEntBh3EcOIfj2M8rg1MzJv+0mHZOIjByawikad\n" + + "BVgEXaWc8gEMANYwv1xsYyunXYK0X1vY/rP1NNPvhLyLIE7NpK90YNBj+xS1ldGD\n" + + "bUdZqZeef2xJe8gMQg05DoD1DF3GipZ0Ies65beh+d5hegb7N4pzh0LzrBrVNHar\n" + + "29b5ExdI7i4iYD5TO6Vr/qTUOiAN/byqELEzAb+L+b2DVz/RoCm4PIp1DU9ewcc2\n" + + "WB38Ofqut3nLYA5tqJ9XvAiEQme+qAVcM3ZFcaMt4I4dXhDZZNg+D9LiTWcxdUPB\n" + + "leu8iwDRjAgyAhPzpFp+nWoqWA81uIiULWD1Fj+IVoY3ZvgivoYOiEFBJ9lbb4te\n" + + "g9m5UT/AaVDTWuHzbspVlbiVe+qyB77C2daWzNyx6UYBPLOo4r0t0c91kbNE5lgj\n" + + "Z7xz6los0N1U8vq91EFSeQJoSQ62XWavYmlCLmdNT6BNfgh4icLsT7Vr1QMX9jzn\n" + + "JtTPxdXytSdHvpSpULsqJ016l0dtmONcK3z9mj5N5z0k1tg1AH970TGYOe2aUcSx\n" + + "IRDMXDOPyzEfjwARAQABAAv9F2CwsjS+Sjh1M1vegJbZjei4gF1HHpEM0K0PSXsp\n" + + "SfVvpR4AoSJ4He6CXSMWg0ot8XKtDuZoV9jnJaES5UL9pMAD7JwIOqZm/DYVJM5h\n" + + "OASCh1c356/wSbFbzRHPtUdZO9Q30WFNJM5pHbCJPjtNoRmRGkf71RxtvHBzy7np\n" + + "Ga+W6U/NVKHw0i0CYwMI0YlKDakYW3Pm+QL+gHZFvngGweTod0f9l2VLLAmeQR/c\n" + + "+EZs7lNumhuZ8mXcwhUc9JQIhOkpO+wreDysEFkAcsKbkQP3UDUsA1gFx9pbMzT0\n" + + "tr1oZq2a4QBtxShHzP/ph7KLpN+6qtjks3xB/yjTgaGmtrwM8tSe0wD1RwXS+/1o\n" + + "BHpXTnQ7TfeOGUAu4KCoOQLv6ELpKWbRBLWuiPwMdbGpvVFALO8+kvKAg9/r+/ny\n" + + "zM2GQHY+J3Jh5JxPiJnHfXNZjIKLbFbIPdSKNyJBuazXW8xIa//mEHMI5OcvsZBK\n" + + "clAIp7LXzjEjKXIwHwDcTn9pBgDpdOKTHOtJ3JUKx0rWVsDH6wq6iKV/FTVSY5jl\n" + + "zN+puOEsskF1Lfxn9JsJihAVO3yNsp6RvkKtyNlFazaCVKtDAmkjoh60XNxcNRqr\n" + + "gCnwdpbgdHP6v/hvZY54ZaJjz6L2e8unNEkYLxDt8cmAyGPgH2XgL7giHIp9jrsQ\n" + + "aS381gnYwNX6wE1aEikgtY91nqJjwPlibF9avSyYQoMtEqM/1UjTjB2KdD/MitK5\n" + + "fP0VpvuXpNYZedmyq4UOMwdkiNMGAOrfmOeT0olgLrTMT5H97Cn3Yxbk13uXHNu/\n" + + "ZUZZNe8s+QtuLfUlKAJtLEUutN33TlWQY522FV0m17S+b80xJib3yZVJteVurrh5\n" + + "HSWHAM+zghQAvCesg5CLXa2dNMkTCmZKgCBvfDLZuZbjFwnwCI6u/NhOY9egKuUf\n" + + "SA/je/RXaT8m5VxLYMxwqQXKApzD87fv0tLPlVIEvjEsaf992tFEFSNPcG1l/jpd\n" + + "5AVXw6kKuf85UkJtYR1x2MkQDrqY1QX/XMw00kt8y9kMZUre19aCArcmor+hDhRJ\n" + + "E3Gt4QJrD9z/bICESw4b4z2DbgD/Xz9IXsA/r9cKiM1h5QMtXvuhyfVeM01enhxM\n" + + "GbOH3gjqqGNKysx0UODGEwr6AV9hAd8RWXMchJLaExK9J5SRawSg671ObAU24SdY\n" + + "vMQ9Z4kAQ2+1ReUZzf3ogSMRZtMT+d18gT6L90/y+APZIaoArLPhebIAGq39HLmJ\n" + + "26x3z0WAgrpA1kNsjXEXkoiZGPLKIGoe3hqJAbYEGAEKACAWIQTRpm4aI7GCyZgP\n" + + "eIz7/MgqAV5zMAUCXaWc8gIbDAAKCRD7/MgqAV5zMOn/C/9ugt+HZIwX308zI+QX\n" + + "c5vDLReuzmJ3ieE0DMO/uNSC+K1XEioSIZP91HeZJ2kbT9nn9fuReuoff0T0Dief\n" + + "rbwcIQQHFFkrqSp1K3VWmUGp2JrUsXFVdjy/fkBIjTd7c5boWljv/6wAsSfiv2V0\n" + + "JSM8EFU6TYXxswGjFVfc6X97tJNeIrXL+mpSmPPqy2bztcCCHkWS5lNLWQw+R7Vg\n" + + "71Fe6yBSNVrqC2/imYG2J9zlowjx1XU63Wdgqp2Wxt0l8OmsB/W80S1fRF5G4SDH\n" + + "s9HXglXXqPsBRZJYfP+VStm9L5P/sKjCcX6WtZR7yS6G8zj/X767MLK/djANvpPd\n" + + "NVniEke6hM3CNBXYPAMhQBMWhCulcoz+0lxi8L34rMN+Dsbma96psdUrn7uLaB91\n" + + "6we0CTfF8qqm7BsVAgalon/UUiuMY80U3ueoj3okiSTiHIjD/YtpXSPioC8nMng7\n" + + "xqAY9Bwizt4FWgXuLm1a4+So4V9j1TRCXd12Uc2l2RNmgDE=\n" + + "=miES\n" + + "-----END PGP PRIVATE KEY BLOCK-----\n"; + + private static final String BOB_CERT = "-----BEGIN PGP PUBLIC KEY BLOCK-----\n" + + "Comment: Bob's OpenPGP certificate\n" + + "\n" + + "mQGNBF2lnPIBDAC5cL9PQoQLTMuhjbYvb4Ncuuo0bfmgPRFywX53jPhoFf4Zg6mv\n" + + "/seOXpgecTdOcVttfzC8ycIKrt3aQTiwOG/ctaR4Bk/t6ayNFfdUNxHWk4WCKzdz\n" + + "/56fW2O0F23qIRd8UUJp5IIlN4RDdRCtdhVQIAuzvp2oVy/LaS2kxQoKvph/5pQ/\n" + + "5whqsyroEWDJoSV0yOb25B/iwk/pLUFoyhDG9bj0kIzDxrEqW+7Ba8nocQlecMF3\n" + + "X5KMN5kp2zraLv9dlBBpWW43XktjcCZgMy20SouraVma8Je/ECwUWYUiAZxLIlMv\n" + + "9CurEOtxUw6N3RdOtLmYZS9uEnn5y1UkF88o8Nku890uk6BrewFzJyLAx5wRZ4F0\n" + + "qV/yq36UWQ0JB/AUGhHVPdFf6pl6eaxBwT5GXvbBUibtf8YI2og5RsgTWtXfU7eb\n" + + "SGXrl5ZMpbA6mbfhd0R8aPxWfmDWiIOhBufhMCvUHh1sApMKVZnvIff9/0Dca3wb\n" + + "vLIwa3T4CyshfT0AEQEAAbQhQm9iIEJhYmJhZ2UgPGJvYkBvcGVucGdwLmV4YW1w\n" + + "bGU+iQHOBBMBCgA4AhsDBQsJCAcCBhUKCQgLAgQWAgMBAh4BAheAFiEE0aZuGiOx\n" + + "gsmYD3iM+/zIKgFeczAFAl2lnvoACgkQ+/zIKgFeczBvbAv/VNk90a6hG8Od9xTz\n" + + "XxH5YRFUSGfIA1yjPIVOnKqhMwps2U+sWE3urL+MvjyQRlyRV8oY9IOhQ5Esm6DO\n" + + "ZYrTnE7qVETm1ajIAP2OFChEc55uH88x/anpPOXOJY7S8jbn3naC9qad75BrZ+3g\n" + + "9EBUWiy5p8TykP05WSnSxNRt7vFKLfEB4nGkehpwHXOVF0CRNwYle42bg8lpmdXF\n" + + "DcCZCi+qEbafmTQzkAqyzS3nCh3IAqq6Y0kBuaKLm2tSNUOlZbD+OHYQNZ5Jix7c\n" + + "ZUzs6Xh4+I55NRWl5smrLq66yOQoFPy9jot/Qxikx/wP3MsAzeGaZSEPc0fHp5G1\n" + + "6rlGbxQ3vl8/usUV7W+TMEMljgwd5x8POR6HC8EaCDfVnUBCPi/Gv+egLjsIbPJZ\n" + + "ZEroiE40e6/UoCiQtlpQB5exPJYSd1Q1txCwueih99PHepsDhmUQKiACszNU+RRo\n" + + "zAYau2VdHqnRJ7QYdxHDiH49jPK4NTMyb/tJh2TiIwcmsIpGuQGNBF2lnPIBDADW\n" + + "ML9cbGMrp12CtF9b2P6z9TTT74S8iyBOzaSvdGDQY/sUtZXRg21HWamXnn9sSXvI\n" + + "DEINOQ6A9QxdxoqWdCHrOuW3ofneYXoG+zeKc4dC86wa1TR2q9vW+RMXSO4uImA+\n" + + "Uzula/6k1DogDf28qhCxMwG/i/m9g1c/0aApuDyKdQ1PXsHHNlgd/Dn6rrd5y2AO\n" + + "baifV7wIhEJnvqgFXDN2RXGjLeCOHV4Q2WTYPg/S4k1nMXVDwZXrvIsA0YwIMgIT\n" + + "86Rafp1qKlgPNbiIlC1g9RY/iFaGN2b4Ir6GDohBQSfZW2+LXoPZuVE/wGlQ01rh\n" + + "827KVZW4lXvqsge+wtnWlszcselGATyzqOK9LdHPdZGzROZYI2e8c+paLNDdVPL6\n" + + "vdRBUnkCaEkOtl1mr2JpQi5nTU+gTX4IeInC7E+1a9UDF/Y85ybUz8XV8rUnR76U\n" + + "qVC7KidNepdHbZjjXCt8/Zo+Tec9JNbYNQB/e9ExmDntmlHEsSEQzFwzj8sxH48A\n" + + "EQEAAYkBtgQYAQoAIBYhBNGmbhojsYLJmA94jPv8yCoBXnMwBQJdpZzyAhsMAAoJ\n" + + "EPv8yCoBXnMw6f8L/26C34dkjBffTzMj5Bdzm8MtF67OYneJ4TQMw7+41IL4rVcS\n" + + "KhIhk/3Ud5knaRtP2ef1+5F66h9/RPQOJ5+tvBwhBAcUWSupKnUrdVaZQanYmtSx\n" + + "cVV2PL9+QEiNN3tzluhaWO//rACxJ+K/ZXQlIzwQVTpNhfGzAaMVV9zpf3u0k14i\n" + + "tcv6alKY8+rLZvO1wIIeRZLmU0tZDD5HtWDvUV7rIFI1WuoLb+KZgbYn3OWjCPHV\n" + + "dTrdZ2CqnZbG3SXw6awH9bzRLV9EXkbhIMez0deCVdeo+wFFklh8/5VK2b0vk/+w\n" + + "qMJxfpa1lHvJLobzOP9fvrswsr92MA2+k901WeISR7qEzcI0Fdg8AyFAExaEK6Vy\n" + + "jP7SXGLwvfisw34OxuZr3qmx1Sufu4toH3XrB7QJN8XyqqbsGxUCBqWif9RSK4xj\n" + + "zRTe56iPeiSJJOIciMP9i2ldI+KgLycyeDvGoBj0HCLO3gVaBe4ubVrj5KjhX2PV\n" + + "NEJd3XZRzaXZE2aAMQ==\n" + + "=NXei\n" + + "-----END PGP PUBLIC KEY BLOCK-----\n"; + + @Test + public void regressionTest() throws IOException { + SOPImpl sop = new SOPImpl(); + byte[] msg = "Hello, World!\n".getBytes(); + Ready encryption = sop.encrypt() + .signWith(CAROL_KEY.getBytes()) + .withCert(BOB_CERT.getBytes()) + .plaintext(msg); + byte[] ciphertext = encryption.getBytes(); + + ByteArrayAndResult decryption = sop.decrypt() + .withKey(BOB_KEY.getBytes()) + .verifyWithCert(CAROL_CERT.getBytes()) + .ciphertext(ciphertext) + .toByteArrayAndResult(); + + byte[] plaintext = decryption.getBytes(); + assertArrayEquals(msg, plaintext); + assertEquals(1, decryption.getResult().getVerifications().size()); + } +} From 23130b6c8aec7aa39833d49424332e0a0e4ad941 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 13 Dec 2022 16:25:44 +0100 Subject: [PATCH 182/763] PGPainless 1.3.14 --- CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0a335092..8e5b5ebe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -46,6 +46,11 @@ SPDX-License-Identifier: CC0-1.0 - Add `KeyRingUtils.publicKeys(PGPKeyRing keys)` - Remove `BCUtil` class +## 1.3.14 +- Bump `bcpg` to `1.72.3` +- Fix DSA key parameter check +- Use proper method to unlock private signing keys when creating detached signatures + ## 1.3.13 - Bump `sop-java` to `4.0.7` From 66abd5f65fb4bfc9ab32f06faa2f988d300e66ad Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 13 Dec 2022 16:42:51 +0100 Subject: [PATCH 183/763] Cleartext-signatures MUST use TEXT mode --- .../src/main/java/org/pgpainless/sop/InlineSignImpl.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/InlineSignImpl.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/InlineSignImpl.java index 30e1d71e..82c3603b 100644 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/InlineSignImpl.java +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/InlineSignImpl.java @@ -75,7 +75,7 @@ public class InlineSignImpl implements InlineSign { for (PGPSecretKeyRing key : signingKeys) { try { if (mode == InlineSignAs.clearsigned) { - signingOptions.addDetachedSignature(protector, key); + signingOptions.addDetachedSignature(protector, key, DocumentSignatureType.CANONICAL_TEXT_DOCUMENT); } else { signingOptions.addInlineSignature(protector, key, modeToSigType(mode)); } From 2d46fb18f775d6298b95d3f2dcd331d573b45e08 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 13 Dec 2022 17:02:53 +0100 Subject: [PATCH 184/763] SOP: Allow generation of keys without user-ids --- .../key/generation/KeyRingTemplates.java | 53 ++++++++++--------- .../org/pgpainless/sop/GenerateKeyImpl.java | 9 ++-- .../org/pgpainless/sop/GenerateKeyTest.java | 7 --- 3 files changed, 32 insertions(+), 37 deletions(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/generation/KeyRingTemplates.java b/pgpainless-core/src/main/java/org/pgpainless/key/generation/KeyRingTemplates.java index 444e7d74..42eb7efa 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/key/generation/KeyRingTemplates.java +++ b/pgpainless-core/src/main/java/org/pgpainless/key/generation/KeyRingTemplates.java @@ -7,6 +7,7 @@ package org.pgpainless.key.generation; import java.security.InvalidAlgorithmParameterException; import java.security.NoSuchAlgorithmException; import javax.annotation.Nonnull; +import javax.annotation.Nullable; import org.bouncycastle.openpgp.PGPException; import org.bouncycastle.openpgp.PGPSecretKeyRing; @@ -38,9 +39,9 @@ public final class KeyRingTemplates { * @throws NoSuchAlgorithmException in case of missing algorithm implementation in the crypto provider * @throws PGPException in case of an OpenPGP related error */ - public PGPSecretKeyRing simpleRsaKeyRing(@Nonnull UserId userId, @Nonnull RsaLength length) + public PGPSecretKeyRing simpleRsaKeyRing(@Nullable UserId userId, @Nonnull RsaLength length) throws InvalidAlgorithmParameterException, NoSuchAlgorithmException, PGPException { - return simpleRsaKeyRing(userId.toString(), length); + return simpleRsaKeyRing(userId == null ? null : userId.toString(), length); } /** @@ -56,7 +57,7 @@ public final class KeyRingTemplates { * @throws NoSuchAlgorithmException in case of missing algorithm implementation in the crypto provider * @throws PGPException in case of an OpenPGP related error */ - public PGPSecretKeyRing simpleRsaKeyRing(@Nonnull String userId, @Nonnull RsaLength length) + public PGPSecretKeyRing simpleRsaKeyRing(@Nullable String userId, @Nonnull RsaLength length) throws InvalidAlgorithmParameterException, NoSuchAlgorithmException, PGPException { return simpleRsaKeyRing(userId, length, Passphrase.emptyPassphrase()); } @@ -75,9 +76,9 @@ public final class KeyRingTemplates { * @throws NoSuchAlgorithmException in case of missing algorithm implementation in the crypto provider * @throws PGPException in case of an OpenPGP related error */ - public PGPSecretKeyRing simpleRsaKeyRing(@Nonnull UserId userId, @Nonnull RsaLength length, String password) + public PGPSecretKeyRing simpleRsaKeyRing(@Nullable UserId userId, @Nonnull RsaLength length, @Nullable String password) throws InvalidAlgorithmParameterException, NoSuchAlgorithmException, PGPException { - return simpleRsaKeyRing(userId.toString(), length, password); + return simpleRsaKeyRing(userId == null ? null : userId.toString(), length, password); } /** @@ -94,7 +95,7 @@ public final class KeyRingTemplates { * @throws NoSuchAlgorithmException in case of missing algorithm implementation in the crypto provider * @throws PGPException in case of an OpenPGP related error */ - public PGPSecretKeyRing simpleRsaKeyRing(@Nonnull String userId, @Nonnull RsaLength length, String password) + public PGPSecretKeyRing simpleRsaKeyRing(@Nullable String userId, @Nonnull RsaLength length, @Nullable String password) throws PGPException, NoSuchAlgorithmException, InvalidAlgorithmParameterException { Passphrase passphrase = Passphrase.emptyPassphrase(); if (!isNullOrEmpty(password)) { @@ -103,12 +104,14 @@ public final class KeyRingTemplates { return simpleRsaKeyRing(userId, length, passphrase); } - public PGPSecretKeyRing simpleRsaKeyRing(@Nonnull String userId, @Nonnull RsaLength length, @Nonnull Passphrase passphrase) + public PGPSecretKeyRing simpleRsaKeyRing(@Nullable String userId, @Nonnull RsaLength length, @Nonnull Passphrase passphrase) throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException { KeyRingBuilder builder = PGPainless.buildKeyRing() .setPrimaryKey(KeySpec.getBuilder(KeyType.RSA(length), KeyFlag.CERTIFY_OTHER, KeyFlag.SIGN_DATA, KeyFlag.ENCRYPT_COMMS)) - .addUserId(userId) .setPassphrase(passphrase); + if (userId != null) { + builder.addUserId(userId); + } return builder.build(); } @@ -125,9 +128,9 @@ public final class KeyRingTemplates { * @throws NoSuchAlgorithmException in case of missing algorithm implementation in the crypto provider * @throws PGPException in case of an OpenPGP related error */ - public PGPSecretKeyRing simpleEcKeyRing(@Nonnull UserId userId) + public PGPSecretKeyRing simpleEcKeyRing(@Nullable UserId userId) throws InvalidAlgorithmParameterException, NoSuchAlgorithmException, PGPException { - return simpleEcKeyRing(userId.toString()); + return simpleEcKeyRing(userId == null ? null : userId.toString()); } /** @@ -143,7 +146,7 @@ public final class KeyRingTemplates { * @throws NoSuchAlgorithmException in case of missing algorithm implementation in the crypto provider * @throws PGPException in case of an OpenPGP related error */ - public PGPSecretKeyRing simpleEcKeyRing(@Nonnull String userId) + public PGPSecretKeyRing simpleEcKeyRing(@Nullable String userId) throws InvalidAlgorithmParameterException, NoSuchAlgorithmException, PGPException { return simpleEcKeyRing(userId, Passphrase.emptyPassphrase()); } @@ -162,9 +165,9 @@ public final class KeyRingTemplates { * @throws NoSuchAlgorithmException in case of missing algorithm implementation in the crypto provider * @throws PGPException in case of an OpenPGP related error */ - public PGPSecretKeyRing simpleEcKeyRing(@Nonnull UserId userId, String password) + public PGPSecretKeyRing simpleEcKeyRing(@Nullable UserId userId, String password) throws InvalidAlgorithmParameterException, NoSuchAlgorithmException, PGPException { - return simpleEcKeyRing(userId.toString(), password); + return simpleEcKeyRing(userId == null ? null : userId.toString(), password); } /** @@ -181,7 +184,7 @@ public final class KeyRingTemplates { * @throws NoSuchAlgorithmException in case of missing algorithm implementation in the crypto provider * @throws PGPException in case of an OpenPGP related error */ - public PGPSecretKeyRing simpleEcKeyRing(@Nonnull String userId, String password) + public PGPSecretKeyRing simpleEcKeyRing(@Nullable String userId, String password) throws PGPException, NoSuchAlgorithmException, InvalidAlgorithmParameterException { Passphrase passphrase = Passphrase.emptyPassphrase(); if (!isNullOrEmpty(password)) { @@ -190,13 +193,15 @@ public final class KeyRingTemplates { return simpleEcKeyRing(userId, passphrase); } - public PGPSecretKeyRing simpleEcKeyRing(@Nonnull String userId, @Nonnull Passphrase passphrase) + public PGPSecretKeyRing simpleEcKeyRing(@Nullable String userId, @Nonnull Passphrase passphrase) throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException { KeyRingBuilder builder = PGPainless.buildKeyRing() .setPrimaryKey(KeySpec.getBuilder(KeyType.EDDSA(EdDSACurve._Ed25519), KeyFlag.CERTIFY_OTHER, KeyFlag.SIGN_DATA)) .addSubkey(KeySpec.getBuilder(KeyType.XDH(XDHSpec._X25519), KeyFlag.ENCRYPT_STORAGE, KeyFlag.ENCRYPT_COMMS)) - .addUserId(userId) .setPassphrase(passphrase); + if (userId != null) { + builder.addUserId(userId); + } return builder.build(); } @@ -211,8 +216,8 @@ public final class KeyRingTemplates { * @throws NoSuchAlgorithmException in case of missing algorithm implementation in the crypto provider * @throws PGPException in case of an OpenPGP related error */ - public PGPSecretKeyRing modernKeyRing(String userId) throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException { - return modernKeyRing(userId, (Passphrase) null); + public PGPSecretKeyRing modernKeyRing(@Nullable String userId) throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException { + return modernKeyRing(userId, Passphrase.emptyPassphrase()); } /** @@ -227,21 +232,21 @@ public final class KeyRingTemplates { * @throws NoSuchAlgorithmException in case of missing algorithm implementation in the crypto provider * @throws PGPException in case of an OpenPGP related error */ - public PGPSecretKeyRing modernKeyRing(String userId, String password) + public PGPSecretKeyRing modernKeyRing(@Nullable String userId, @Nullable String password) throws InvalidAlgorithmParameterException, NoSuchAlgorithmException, PGPException { - Passphrase passphrase = (password != null ? Passphrase.fromPassword(password) : null); + Passphrase passphrase = (password != null ? Passphrase.fromPassword(password) : Passphrase.emptyPassphrase()); return modernKeyRing(userId, passphrase); } - public PGPSecretKeyRing modernKeyRing(String userId, Passphrase passphrase) + public PGPSecretKeyRing modernKeyRing(@Nullable String userId, @Nonnull Passphrase passphrase) throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException { KeyRingBuilder builder = PGPainless.buildKeyRing() .setPrimaryKey(KeySpec.getBuilder(KeyType.EDDSA(EdDSACurve._Ed25519), KeyFlag.CERTIFY_OTHER)) .addSubkey(KeySpec.getBuilder(KeyType.XDH(XDHSpec._X25519), KeyFlag.ENCRYPT_STORAGE, KeyFlag.ENCRYPT_COMMS)) .addSubkey(KeySpec.getBuilder(KeyType.EDDSA(EdDSACurve._Ed25519), KeyFlag.SIGN_DATA)) - .addUserId(userId); - if (passphrase != null && !passphrase.isEmpty()) { - builder.setPassphrase(passphrase); + .setPassphrase(passphrase); + if (userId != null) { + builder.addUserId(userId); } return builder.build(); } diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/GenerateKeyImpl.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/GenerateKeyImpl.java index 893c8dd8..c75087c8 100644 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/GenerateKeyImpl.java +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/GenerateKeyImpl.java @@ -28,7 +28,7 @@ public class GenerateKeyImpl implements GenerateKey { private boolean armor = true; private final Set userIds = new LinkedHashSet<>(); - private Passphrase passphrase; + private Passphrase passphrase = Passphrase.emptyPassphrase(); @Override public GenerateKey noArmor() { @@ -51,14 +51,11 @@ public class GenerateKeyImpl implements GenerateKey { @Override public Ready generate() throws SOPGPException.MissingArg, SOPGPException.UnsupportedAsymmetricAlgo { Iterator userIdIterator = userIds.iterator(); - if (!userIdIterator.hasNext()) { - throw new SOPGPException.MissingArg("Missing user-id."); - } - PGPSecretKeyRing key; try { + String primaryUserId = userIdIterator.hasNext() ? userIdIterator.next() : null; key = PGPainless.generateKeyRing() - .modernKeyRing(userIdIterator.next(), passphrase); + .modernKeyRing(primaryUserId, passphrase); if (userIdIterator.hasNext()) { SecretKeyRingEditorInterface editor = PGPainless.modifyKeyRing(key); diff --git a/pgpainless-sop/src/test/java/org/pgpainless/sop/GenerateKeyTest.java b/pgpainless-sop/src/test/java/org/pgpainless/sop/GenerateKeyTest.java index 7f1710fd..a71eda12 100644 --- a/pgpainless-sop/src/test/java/org/pgpainless/sop/GenerateKeyTest.java +++ b/pgpainless-sop/src/test/java/org/pgpainless/sop/GenerateKeyTest.java @@ -6,7 +6,6 @@ package org.pgpainless.sop; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import java.io.IOException; @@ -17,7 +16,6 @@ import org.junit.jupiter.api.Test; import org.pgpainless.PGPainless; import org.pgpainless.key.info.KeyRingInfo; import sop.SOP; -import sop.exception.SOPGPException; public class GenerateKeyTest { @@ -28,11 +26,6 @@ public class GenerateKeyTest { sop = new SOPImpl(); } - @Test - public void testMissingUserId() { - assertThrows(SOPGPException.MissingArg.class, () -> sop.generateKey().generate()); - } - @Test public void generateKey() throws IOException { byte[] bytes = sop.generateKey() From cfba77dea5eb75bf5fdf9ec255e196f338d0fe11 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 14 Dec 2022 00:11:23 +0100 Subject: [PATCH 185/763] Update changelog --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8e5b5ebe..ac12a0f1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,10 @@ SPDX-License-Identifier: CC0-1.0 # PGPainless Changelog +## 1.4.0-rc3-SNAPSHOT +- `sop generate-key`: Add support for keys without user-ids +- `sop inline-sign --as=clearsigned`: Make signature in TEXT mode + ## 1.4.0-rc2 - Bump `bcpg-jdk15to18` to `1.72.3` - Use BCs `PGPEncryptedDataList.extractSessionKeyEncryptedData()` method From bfbaa30e4cb5567078098a080055cf6cf3b376f7 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 15 Dec 2022 16:12:30 +0100 Subject: [PATCH 186/763] Make KO-countermeasures configurable (off by default) --- .../key/protection/UnlockSecretKey.java | 5 +++- .../java/org/pgpainless/policy/Policy.java | 29 +++++++++++++++++++ .../ModifiedPublicKeysInvestigation.java | 6 ++++ 3 files changed, 39 insertions(+), 1 deletion(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/protection/UnlockSecretKey.java b/pgpainless-core/src/main/java/org/pgpainless/key/protection/UnlockSecretKey.java index 42a5b9b7..78c849ab 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/key/protection/UnlockSecretKey.java +++ b/pgpainless-core/src/main/java/org/pgpainless/key/protection/UnlockSecretKey.java @@ -9,6 +9,7 @@ import org.bouncycastle.openpgp.PGPException; import org.bouncycastle.openpgp.PGPPrivateKey; import org.bouncycastle.openpgp.PGPSecretKey; import org.bouncycastle.openpgp.operator.PBESecretKeyDecryptor; +import org.pgpainless.PGPainless; import org.pgpainless.exception.KeyIntegrityException; import org.pgpainless.exception.WrongPassphraseException; import org.pgpainless.key.info.KeyInfo; @@ -51,7 +52,9 @@ public final class UnlockSecretKey { throw new PGPException("Cannot decrypt secret key."); } - PublicKeyParameterValidationUtil.verifyPublicKeyParameterIntegrity(privateKey, secretKey.getPublicKey()); + if (PGPainless.getPolicy().isEnableKeyParameterValidation()) { + PublicKeyParameterValidationUtil.verifyPublicKeyParameterIntegrity(privateKey, secretKey.getPublicKey()); + } return privateKey; } diff --git a/pgpainless-core/src/main/java/org/pgpainless/policy/Policy.java b/pgpainless-core/src/main/java/org/pgpainless/policy/Policy.java index ad9f0635..3556f104 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/policy/Policy.java +++ b/pgpainless-core/src/main/java/org/pgpainless/policy/Policy.java @@ -49,6 +49,8 @@ public final class Policy { // Signers User-ID is soon to be deprecated. private SignerUserIdValidationLevel signerUserIdValidationLevel = SignerUserIdValidationLevel.DISABLED; + private boolean enableKeyParameterValidation = false; + public enum SignerUserIdValidationLevel { /** * PGPainless will verify {@link org.bouncycastle.bcpg.sig.SignerUserID} subpackets in signatures strictly. @@ -701,4 +703,31 @@ public final class Policy { this.signerUserIdValidationLevel = signerUserIdValidationLevel; return this; } + + /** + * Enable or disable validation of public key parameters when unlocking private keys. + * Disabled by default. + * When enabled, PGPainless will validate, whether public key parameters have been tampered with. + * This is a countermeasure against possible attacks described in the paper + * "Victory by KO: Attacking OpenPGP Using Key Overwriting" by Lara Bruseghini, Daniel Huigens, and Kenneth G. Paterson. + * Since these attacks are only possible in very special conditions (attacker has access to the encrypted private key), + * and the countermeasures are very costly, they are disabled by default, but can be enabled using this method. + * + * @see KOpenPGP.com + * @param enable boolean + * @return this + */ + public Policy setEnableKeyParameterValidation(boolean enable) { + this.enableKeyParameterValidation = enable; + return this; + } + + /** + * Return true, if countermeasures against the KOpenPGP attacks are enabled, false otherwise. + * + * @return true if countermeasures are enabled, false otherwise. + */ + public boolean isEnableKeyParameterValidation() { + return enableKeyParameterValidation; + } } diff --git a/pgpainless-core/src/test/java/investigations/ModifiedPublicKeysInvestigation.java b/pgpainless-core/src/test/java/investigations/ModifiedPublicKeysInvestigation.java index ba16e2d0..6930f78f 100644 --- a/pgpainless-core/src/test/java/investigations/ModifiedPublicKeysInvestigation.java +++ b/pgpainless-core/src/test/java/investigations/ModifiedPublicKeysInvestigation.java @@ -212,10 +212,12 @@ public class ModifiedPublicKeysInvestigation { SecretKeyRingProtector protector = SecretKeyRingProtector.unlockAnyKeyWith(Passphrase.fromPassword("12345678")); PGPSecretKeyRing dsa = PGPainless.readKeyRing().secretKeyRing(DSA); + PGPainless.getPolicy().setEnableKeyParameterValidation(true); assertThrows(KeyIntegrityException.class, () -> UnlockSecretKey.unlockSecretKey(dsa.getSecretKey(KeyIdUtil.fromLongKeyId("b1bd1f049ec87f3d")), protector)); assertThrows(KeyIntegrityException.class, () -> UnlockSecretKey.unlockSecretKey(dsa.getSecretKey(KeyIdUtil.fromLongKeyId("f5ffdf6d71dd5789")), protector)); + PGPainless.getPolicy().setEnableKeyParameterValidation(false); } @Test @@ -223,8 +225,10 @@ public class ModifiedPublicKeysInvestigation { SecretKeyRingProtector protector = SecretKeyRingProtector.unlockAnyKeyWith(Passphrase.fromPassword("12345678")); PGPSecretKeyRing elgamal = PGPainless.readKeyRing().secretKeyRing(ELGAMAL); + PGPainless.getPolicy().setEnableKeyParameterValidation(true); assertThrows(KeyIntegrityException.class, () -> UnlockSecretKey.unlockSecretKey(elgamal.getSecretKey(KeyIdUtil.fromLongKeyId("f5ffdf6d71dd5789")), protector)); + PGPainless.getPolicy().setEnableKeyParameterValidation(false); } @Test @@ -232,8 +236,10 @@ public class ModifiedPublicKeysInvestigation { PGPSecretKeyRing secretKeys = PGPainless.readKeyRing().secretKeyRing(INJECTED_KEY); SecretKeyRingProtector protector = SecretKeyRingProtector.unlockAnyKeyWith(Passphrase.fromPassword("pass")); + PGPainless.getPolicy().setEnableKeyParameterValidation(true); assertThrows(KeyIntegrityException.class, () -> UnlockSecretKey.unlockSecretKey(secretKeys.getSecretKey(), protector)); + PGPainless.getPolicy().setEnableKeyParameterValidation(false); } @Test From 6a5c6c55096ea8ac99587accc3120b72d9dcfe23 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 15 Dec 2022 16:28:10 +0100 Subject: [PATCH 187/763] Improve ElGamal validation by refraining from biginteger for loop variable --- .../key/util/PublicKeyParameterValidationUtil.java | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/util/PublicKeyParameterValidationUtil.java b/pgpainless-core/src/main/java/org/pgpainless/key/util/PublicKeyParameterValidationUtil.java index 88e9897a..344f063b 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/key/util/PublicKeyParameterValidationUtil.java +++ b/pgpainless-core/src/main/java/org/pgpainless/key/util/PublicKeyParameterValidationUtil.java @@ -265,14 +265,15 @@ public class PublicKeyParameterValidationUtil { // check g^i mod p != 1 for i < threshold BigInteger res = g; - BigInteger i = BigInteger.valueOf(1); - BigInteger threshold = BigInteger.valueOf(2).shiftLeft(17); - while (i.compareTo(threshold) < 0) { + // 262144 + int threshold = 2 << 17; + int i = 1; + while (i < threshold) { res = res.multiply(g).mod(p); if (res.equals(one)) { return false; } - i = i.add(one); + i++; } // blinded exponentiation to check y = g^(r*(p-1)+x) mod p From 3f10efac7a8f561da5ce5f503020b6ab1fa78d28 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 15 Dec 2022 18:06:50 +0100 Subject: [PATCH 188/763] Update changelog --- CHANGELOG.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ac12a0f1..bb449799 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,9 @@ SPDX-License-Identifier: CC0-1.0 ## 1.4.0-rc3-SNAPSHOT - `sop generate-key`: Add support for keys without user-ids - `sop inline-sign --as=clearsigned`: Make signature in TEXT mode +- Make countermeasures against KOpenPGP attacks configurable + - Countermeasures are now disabled by default since they are costly and have a specific threat model + - Can be enabled by calling `Policy.setEnableKeyParameterValidation(true)` ## 1.4.0-rc2 - Bump `bcpg-jdk15to18` to `1.72.3` @@ -50,6 +53,11 @@ SPDX-License-Identifier: CC0-1.0 - Add `KeyRingUtils.publicKeys(PGPKeyRing keys)` - Remove `BCUtil` class +## 1.3.15 +- Fix crash in `sop generate-key --with-key-password` when more than one user-id is given +- `sop generate-key`: Allow key generation without user-ids +- `sop inline-sign --as=clearsigned`: Make signatures of type 'text' instead of 'binary' + ## 1.3.14 - Bump `bcpg` to `1.72.3` - Fix DSA key parameter check From 7a326ddef0e21360cadf519a68450440dacee72e Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 15 Dec 2022 18:19:13 +0100 Subject: [PATCH 189/763] PGPainless 1.4.0 --- CHANGELOG.md | 2 +- README.md | 2 +- pgpainless-sop/README.md | 4 ++-- version.gradle | 4 ++-- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bb449799..a8c57019 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ SPDX-License-Identifier: CC0-1.0 # PGPainless Changelog -## 1.4.0-rc3-SNAPSHOT +## 1.4.0 - `sop generate-key`: Add support for keys without user-ids - `sop inline-sign --as=clearsigned`: Make signature in TEXT mode - Make countermeasures against KOpenPGP attacks configurable diff --git a/README.md b/README.md index 30af42f8..39741bb5 100644 --- a/README.md +++ b/README.md @@ -191,7 +191,7 @@ repositories { } dependencies { - implementation 'org.pgpainless:pgpainless-core:1.4.0-rc2' + implementation 'org.pgpainless:pgpainless-core:1.4.0' } ``` diff --git a/pgpainless-sop/README.md b/pgpainless-sop/README.md index 2e4927a6..e7e612de 100644 --- a/pgpainless-sop/README.md +++ b/pgpainless-sop/README.md @@ -23,7 +23,7 @@ To start using pgpainless-sop in your code, include the following lines in your ... dependencies { ... - implementation "org.pgpainless:pgpainless-sop:1.4.0-rc2" + implementation "org.pgpainless:pgpainless-sop:1.4.0" ... } @@ -34,7 +34,7 @@ dependencies { org.pgpainless pgpainless-sop - 1.4.0-rc2 + 1.4.0 ... diff --git a/version.gradle b/version.gradle index 2d9745dd..8ced1bc0 100644 --- a/version.gradle +++ b/version.gradle @@ -4,8 +4,8 @@ allprojects { ext { - shortVersion = '1.4.0-rc3' - isSnapshot = true + shortVersion = '1.4.0' + isSnapshot = false pgpainlessMinAndroidSdk = 10 javaSourceCompatibility = 1.8 bouncyCastleVersion = '1.72' From 01e6ef0013ab0638e3465f2069032bf39cda6309 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 15 Dec 2022 18:22:04 +0100 Subject: [PATCH 190/763] PGPainless 1.4.1-SNAPSHOT --- version.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/version.gradle b/version.gradle index 8ced1bc0..65038afb 100644 --- a/version.gradle +++ b/version.gradle @@ -4,8 +4,8 @@ allprojects { ext { - shortVersion = '1.4.0' - isSnapshot = false + shortVersion = '1.4.1' + isSnapshot = true pgpainlessMinAndroidSdk = 10 javaSourceCompatibility = 1.8 bouncyCastleVersion = '1.72' From c90b56ba13eac5ade70c64caf6808b52e31187a6 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Fri, 16 Dec 2022 17:20:10 +0100 Subject: [PATCH 191/763] Add YourKit endorsement --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index 39741bb5..6dbceacd 100644 --- a/README.md +++ b/README.md @@ -222,6 +222,9 @@ Parts of PGPainless development ([project page](https://nlnet.nl/project/PGPainl NGI Assure is made possible with financial support from the [European Commission](https://ec.europa.eu/)'s [Next Generation Internet](https://ngi.eu/) programme, under the aegis of [DG Communications Networks, Content and Technology](https://ec.europa.eu/info/departments/communications-networks-content-and-technology_en). [![NGI Assure Logo](https://blog.jabberhead.tk/wp-content/uploads/2022/05/NGIAssure_tag.svg)](https://nlnet.nl/assure/) +Thanks to [YourKit](https://www.yourkit.com/) for providing a free license of the [YourKit Java Profiler](https://www.yourkit.com/java/profiler/) to support PGPainless Development! +[![YourKit Logo](https://www.yourkit.com/images/yklogo.png)](https://www.yourkit.com/) + Big thank you also to those who decided to support the work by donating! Notably @msfjarvis From dbcca586d1c58d931d053ce323dbe189fa1110da Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Fri, 16 Dec 2022 17:34:53 +0100 Subject: [PATCH 192/763] Add link to KOpenPGP to changelog --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a8c57019..4bafa039 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,7 +8,7 @@ SPDX-License-Identifier: CC0-1.0 ## 1.4.0 - `sop generate-key`: Add support for keys without user-ids - `sop inline-sign --as=clearsigned`: Make signature in TEXT mode -- Make countermeasures against KOpenPGP attacks configurable +- Make countermeasures against [KOpenPGP](https://kopenpgp.com/) attacks configurable - Countermeasures are now disabled by default since they are costly and have a specific threat model - Can be enabled by calling `Policy.setEnableKeyParameterValidation(true)` From 59217d25013df10eb8c22c129d7f30201e77bfc0 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 19 Dec 2022 16:25:27 +0100 Subject: [PATCH 193/763] Implement UserId.parse(mailbox) --- .../java/org/pgpainless/key/util/UserId.java | 62 +++++++++- .../java/org/pgpainless/key/UserIdTest.java | 110 ++++++++++++++++++ 2 files changed, 171 insertions(+), 1 deletion(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/util/UserId.java b/pgpainless-core/src/main/java/org/pgpainless/key/util/UserId.java index a1b251dc..6dca06b5 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/key/util/UserId.java +++ b/pgpainless-core/src/main/java/org/pgpainless/key/util/UserId.java @@ -5,10 +5,21 @@ package org.pgpainless.key.util; import java.util.Comparator; +import java.util.regex.Matcher; +import java.util.regex.Pattern; import javax.annotation.Nonnull; import javax.annotation.Nullable; public final class UserId implements CharSequence { + + private static final Pattern emailPattern = Pattern.compile("(?:[\\p{L}\\u0900-\\u097F0-9!#\\$%&'*+/=?^_`{|}~-]+(?:\\.[\\p{L}\\u0900-\\u097F0-9!#\\$%&'*+/=?^_`{|}~-]+)*|\"(?:[\\x01-\\x08\\x0b\\x0c\\x0e-" + + "\\x1f\\x21\\x23-\\x5b\\x5d-\\x7f]|\\\\[\\x01-\\x09\\x0b\\x0c\\x0e-\\x7f])*\")@(?:(?:[\\p{L}\\u0900-\\u097F0-9](?:[\\p{L}\\u0900-\\u097F0-9" + + "-]*[\\p{L}\\u0900-\\u097F0-9])?\\.)+[\\p{L}\\u0900-\\u097F0-9](?:[\\p{L}\\u0900-\\u097F0-9-]*[\\p{L}\\u0900-\\u097F0-9])?|\\[(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)" + + "\\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?|[$\\p{L}\\u0900-\\u097F0-9-]*[\\p{L}\\u0900-\\u097F0-9]:(?:[\\x01-\\x08\\x0b\\x0c\\x0e-\\x1f" + + "\\x21-\\x5a\\x53-\\x7f]|\\\\[\\x01-\\x09\\x0b\\x0c\\x0e-\\x7f])+)])"); + + private static final Pattern nameAddrPattern = Pattern.compile("^((?.+?)\\s)?(\\((?.+?)\\)\\s)?(<(?.+?)>)?$"); + public static final class Builder { private String name; private String comment; @@ -58,6 +69,37 @@ public final class UserId implements CharSequence { } } + public static UserId parse(@Nonnull String string) { + Builder builder = newBuilder(); + string = string.trim(); + Matcher matcher = nameAddrPattern.matcher(string); + if (matcher.find()) { + String name = matcher.group("name"); + String comment = matcher.group("comment"); + String mail = matcher.group("email"); + matcher = emailPattern.matcher(mail); + if (!matcher.matches()) { + throw new IllegalArgumentException("Malformed email address"); + } + + if (name != null) { + builder.withName(name); + } + if (comment != null) { + builder.withComment(comment); + } + builder.withEmail(mail); + } else { + matcher = emailPattern.matcher(string); + if (matcher.matches()) { + builder.withEmail(string); + } else { + throw new IllegalArgumentException("Malformed email address"); + } + } + return builder.build(); + } + private final String name; private final String comment; private final String email; @@ -86,6 +128,24 @@ public final class UserId implements CharSequence { } public String getName() { + return getName(false); + } + + public String getName(boolean preserveQuotes) { + if (name == null || name.isEmpty()) { + return name; + } + + if (name.startsWith("\"")) { + if (preserveQuotes) { + return name; + } + String withoutQuotes = name.substring(1); + if (withoutQuotes.endsWith("\"")) { + withoutQuotes = withoutQuotes.substring(0, withoutQuotes.length() - 1); + } + return withoutQuotes; + } return name; } @@ -116,7 +176,7 @@ public final class UserId implements CharSequence { public @Nonnull String toString() { StringBuilder sb = new StringBuilder(); if (name != null && !name.isEmpty()) { - sb.append(name); + sb.append(getName(true)); } if (comment != null && !comment.isEmpty()) { if (sb.length() > 0) { diff --git a/pgpainless-core/src/test/java/org/pgpainless/key/UserIdTest.java b/pgpainless-core/src/test/java/org/pgpainless/key/UserIdTest.java index acc90918..b910e1b4 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/key/UserIdTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/key/UserIdTest.java @@ -271,4 +271,114 @@ public class UserIdTest { assertNotEquals(0, UserId.compare(id1, id6, c)); assertNotEquals(0, UserId.compare(id6, id1, c)); } + + @Test + public void parseNameAndEmail() { + UserId id = UserId.parse("Alice "); + + assertEquals("Alice", id.getName()); + assertNull(id.getComment()); + assertEquals("alice@pgpainless.org", id.getEmail()); + + assertEquals("Alice ", id.toString()); + } + + @Test + public void parseNameCommentAndEmail() { + UserId id = UserId.parse("Alice (work mail) "); + + assertEquals("Alice", id.getName()); + assertEquals("work mail", id.getComment()); + assertEquals("alice@pgpainless.org", id.getEmail()); + + assertEquals("Alice (work mail) ", id.toString()); + } + + @Test + public void parseLongNameAndEmail() { + UserId id = UserId.parse("Alice von Painleicester "); + + assertEquals("Alice von Painleicester", id.getName()); + assertNull(id.getComment()); + assertEquals("alice@pgpainless.org", id.getEmail()); + + assertEquals("Alice von Painleicester ", id.toString()); + } + + @Test + public void parseLongNameCommentAndEmail() { + UserId id = UserId.parse("Alice von Painleicester (work email) "); + + assertEquals("Alice von Painleicester", id.getName()); + assertEquals("work email", id.getComment()); + assertEquals("alice@pgpainless.org", id.getEmail()); + + assertEquals("Alice von Painleicester (work email) ", id.toString()); + } + + @Test + public void parseQuotedNameAndEmail() { + UserId id = UserId.parse("\"Alice\" "); + + assertEquals("Alice", id.getName()); + assertNull(id.getComment()); + assertEquals("alice@pgpainless.org", id.getEmail()); + + assertEquals("\"Alice\" ", id.toString()); + } + + @Test + public void parseQuotedNameCommentAndEmail() { + UserId id = UserId.parse("\"Alice\" (work email) "); + + assertEquals("Alice", id.getName()); + assertEquals("work email", id.getComment()); + assertEquals("alice@pgpainless.org", id.getEmail()); + + assertEquals("\"Alice\" (work email) ", id.toString()); + } + + @Test + public void parseLongQuotedNameAndEmail() { + UserId id = UserId.parse("\"Alice Mac Painlester\" "); + + assertEquals("Alice Mac Painlester", id.getName()); + assertNull(id.getComment()); + assertEquals("alice@pgpainless.org", id.getEmail()); + + assertEquals("\"Alice Mac Painlester\" ", id.toString()); + } + + @Test + public void parseLongQuotedNameCommentAndEmail() { + UserId id = UserId.parse("\"Alice Mac Painlester\" (work email) "); + + assertEquals("Alice Mac Painlester", id.getName()); + assertEquals("work email", id.getComment()); + assertEquals("alice@pgpainless.org", id.getEmail()); + + assertEquals("\"Alice Mac Painlester\" (work email) ", id.toString()); + } + + @Test + public void parseEmailOnly() { + UserId id = UserId.parse("alice@pgpainless.org"); + + assertNull(id.getName()); + assertNull(id.getComment()); + assertEquals("alice@pgpainless.org", id.getEmail()); + + assertEquals("", id.toString()); + } + + @Test + public void parseBracketedEmailOnly() { + UserId id = UserId.parse(""); + + assertNull(id.getName()); + assertNull(id.getComment()); + assertEquals("alice@pgpainless.org", id.getEmail()); + + assertEquals("", id.toString()); + } } From 94851ccb8f5294941f4715a66d5e4a50467d91d1 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 20 Dec 2022 15:57:11 +0100 Subject: [PATCH 194/763] Add javadoc for UserId.parse() --- .../java/org/pgpainless/key/util/UserId.java | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/util/UserId.java b/pgpainless-core/src/main/java/org/pgpainless/key/util/UserId.java index 6dca06b5..3c43b92c 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/key/util/UserId.java +++ b/pgpainless-core/src/main/java/org/pgpainless/key/util/UserId.java @@ -69,6 +69,24 @@ public final class UserId implements CharSequence { } } + /** + * Parse a {@link UserId} from free-form text,
name-addr
or
mailbox
string and split it + * up into its components. + * Example inputs for this method: + *
    + *
  • john@pgpainless.org
  • + *
  • <john@pgpainless.org>
  • + *
  • John Doe
  • + *
  • John Doe <john@pgpainless.org>
  • + *
  • John Doe (work email) <john@pgpainless.org>
  • + *
+ * In these cases, {@link #parse(String)} will detect email addresses, names and comments and expose those + * via the respective getters. + * + * @see RFC5322 §3.4. Address Specification + * @param string user-id + * @return parsed {@link UserId} object + */ public static UserId parse(@Nonnull String string) { Builder builder = newBuilder(); string = string.trim(); From 75f69c0473c1a2807fc39d0d89ce20c474df79c5 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 20 Dec 2022 17:27:32 +0100 Subject: [PATCH 195/763] Fix Android compatibility by using Matcher.group(int) instead of Matcher.group(String) --- .../src/main/java/org/pgpainless/key/util/UserId.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/util/UserId.java b/pgpainless-core/src/main/java/org/pgpainless/key/util/UserId.java index 3c43b92c..e79d9700 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/key/util/UserId.java +++ b/pgpainless-core/src/main/java/org/pgpainless/key/util/UserId.java @@ -92,9 +92,9 @@ public final class UserId implements CharSequence { string = string.trim(); Matcher matcher = nameAddrPattern.matcher(string); if (matcher.find()) { - String name = matcher.group("name"); - String comment = matcher.group("comment"); - String mail = matcher.group("email"); + String name = matcher.group(2); + String comment = matcher.group(4); + String mail = matcher.group(6); matcher = emailPattern.matcher(mail); if (!matcher.matches()) { throw new IllegalArgumentException("Malformed email address"); From a376587680ecd76b8b3302875731f596e4c44805 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 22 Dec 2022 14:43:09 +0100 Subject: [PATCH 196/763] Add tests for international user-ids --- .../java/org/pgpainless/key/UserIdTest.java | 60 +++++++++++++++++++ 1 file changed, 60 insertions(+) diff --git a/pgpainless-core/src/test/java/org/pgpainless/key/UserIdTest.java b/pgpainless-core/src/test/java/org/pgpainless/key/UserIdTest.java index b910e1b4..aece0efb 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/key/UserIdTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/key/UserIdTest.java @@ -381,4 +381,64 @@ public class UserIdTest { assertEquals("", id.toString()); } + + @Test + public void parseLatinWithDiacritics() { + UserId pele = UserId.parse("Pelé@example.com"); + assertEquals("Pelé@example.com", pele.getEmail()); + + pele = UserId.parse("Marquez Pelé "); + assertEquals("Pelé@example.com", pele.getEmail()); + assertEquals("Marquez Pelé", pele.getName()); + } + + @Test + public void parseGreekAlphabet() { + UserId dokimi = UserId.parse("δοκιμή@παράδειγμα.δοκιμή"); + assertEquals("δοκιμή@παράδειγμα.δοκιμή", dokimi.getEmail()); + + dokimi = UserId.parse("δοκιμή <δοκιμή@παράδειγμα.δοκιμή>"); + assertEquals("δοκιμή", dokimi.getName()); + assertEquals("δοκιμή@παράδειγμα.δοκιμή", dokimi.getEmail()); + } + + @Test + public void parseTraditionalChinese() { + UserId womai = UserId.parse("我買@屋企.香港"); + assertEquals("我買@屋企.香港", womai.getEmail()); + + womai = UserId.parse("我買 <我買@屋企.香港>"); + assertEquals("我買@屋企.香港", womai.getEmail()); + assertEquals("我買", womai.getName()); + } + + @Test + public void parseJapanese() { + UserId ninomiya = UserId.parse("二ノ宮@黒川.日本"); + assertEquals("二ノ宮@黒川.日本", ninomiya.getEmail()); + + ninomiya = UserId.parse("二ノ宮 <二ノ宮@黒川.日本>"); + assertEquals("二ノ宮@黒川.日本", ninomiya.getEmail()); + assertEquals("二ノ宮", ninomiya.getName()); + } + + @Test + public void parseCyrillic() { + UserId medved = UserId.parse("медведь@с-балалайкой.рф"); + assertEquals("медведь@с-балалайкой.рф", medved.getEmail()); + + medved = UserId.parse("медведь <медведь@с-балалайкой.рф>"); + assertEquals("медведь@с-балалайкой.рф", medved.getEmail()); + assertEquals("медведь", medved.getName()); + } + + @Test + public void parseDevanagari() { + UserId samparka = UserId.parse("संपर्क@डाटामेल.भारत"); + assertEquals("संपर्क@डाटामेल.भारत", samparka.getEmail()); + + samparka = UserId.parse("संपर्क <संपर्क@डाटामेल.भारत>"); + assertEquals("संपर्क@डाटामेल.भारत", samparka.getEmail()); + assertEquals("संपर्क", samparka.getName()); + } } From 533b54a6b7c5f1aad340d261fdfcd54fa8cde032 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 22 Dec 2022 15:01:10 +0100 Subject: [PATCH 197/763] Add some more tests for valid email address formats --- .../java/org/pgpainless/key/util/UserId.java | 6 ++ .../java/org/pgpainless/key/UserIdTest.java | 91 +++++++++++++++++++ 2 files changed, 97 insertions(+) diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/util/UserId.java b/pgpainless-core/src/main/java/org/pgpainless/key/util/UserId.java index e79d9700..1a2ea265 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/key/util/UserId.java +++ b/pgpainless-core/src/main/java/org/pgpainless/key/util/UserId.java @@ -82,6 +82,12 @@ public final class UserId implements CharSequence { * * In these cases, {@link #parse(String)} will detect email addresses, names and comments and expose those * via the respective getters. + * This method does not support parsing mail addresses of the following formats: + *
    + *
  • Local domains without TLDs (
    user@localdomain1
    )
  • + *
  • " "@example.org
    (spaces between the quotes)
  • + *
  • "very.(),:;<>[]\".VERY.\"very@\\ \"very\".unusual"@strange.example.com
  • + *
* * @see RFC5322 §3.4. Address Specification * @param string user-id diff --git a/pgpainless-core/src/test/java/org/pgpainless/key/UserIdTest.java b/pgpainless-core/src/test/java/org/pgpainless/key/UserIdTest.java index aece0efb..93cb6922 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/key/UserIdTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/key/UserIdTest.java @@ -441,4 +441,95 @@ public class UserIdTest { assertEquals("संपर्क@डाटामेल.भारत", samparka.getEmail()); assertEquals("संपर्क", samparka.getName()); } + + @Test + public void parseMailWithPlus() { + UserId id = UserId.parse("disposable.style.email.with+symbol@example.com"); + assertEquals("disposable.style.email.with+symbol@example.com", id.getEmail()); + + id = UserId.parse("Disposable Mail "); + assertEquals("disposable.style.email.with+symbol@example.com", id.getEmail()); + assertEquals("Disposable Mail", id.getName()); + } + + @Test + public void parseMailWithHyphen() { + UserId id = UserId.parse("other.email-with-hyphen@example.com"); + assertEquals("other.email-with-hyphen@example.com", id.getEmail()); + + id = UserId.parse("Other Email "); + assertEquals("other.email-with-hyphen@example.com", id.getEmail()); + assertEquals("Other Email", id.getName()); + } + + @Test + public void parseMailWithTagAndSorting() { + UserId id = UserId.parse("user.name+tag+sorting@example.com"); + assertEquals("user.name+tag+sorting@example.com", id.getEmail()); + + id = UserId.parse("User Name "); + assertEquals("user.name+tag+sorting@example.com", id.getEmail()); + assertEquals("User Name", id.getName()); + } + + @Test + public void parseMailWithSlash() { + UserId id = UserId.parse("test/test@test.com"); + assertEquals("test/test@test.com", id.getEmail()); + + id = UserId.parse("Who uses Slashes "); + assertEquals("test/test@test.com", id.getEmail()); + assertEquals("Who uses Slashes", id.getName()); + } + + @Test + public void parseDoubleDots() { + UserId id = UserId.parse("\"john..doe\"@example.org"); + assertEquals("\"john..doe\"@example.org", id.getEmail()); + + id = UserId.parse("John Doe <\"john..doe\"@example.org>"); + assertEquals("\"john..doe\"@example.org", id.getEmail()); + assertEquals("John Doe", id.getName()); + } + + @Test + public void parseBangifiedHostRoute() { + UserId id = UserId.parse("mailhost!username@example.org"); + assertEquals("mailhost!username@example.org", id.getEmail()); + + id = UserId.parse("Bangified Host Route "); + assertEquals("mailhost!username@example.org", id.getEmail()); + assertEquals("Bangified Host Route", id.getName()); + } + + @Test + public void parsePercentRouted() { + UserId id = UserId.parse("user%example.com@example.org"); + assertEquals("user%example.com@example.org", id.getEmail()); + + id = UserId.parse("User "); + assertEquals("user%example.com@example.org", id.getEmail()); + assertEquals("User", id.getName()); + } + + @Test + public void parseLocalPartEndingWithNonAlphanumericCharacter() { + UserId id = UserId.parse("user-@example.org"); + assertEquals("user-@example.org", id.getEmail()); + + id = UserId.parse("User "); + assertEquals("user-@example.org", id.getEmail()); + assertEquals("User", id.getName()); + } + + @Test + public void parseDomainIsIpAddress() { + UserId id = UserId.parse("postmaster@[123.123.123.123]"); + assertEquals("postmaster@[123.123.123.123]", id.getEmail()); + + id = UserId.parse("Alice (work email) "); + assertEquals("postmaster@[123.123.123.123]", id.getEmail()); + assertEquals("Alice", id.getName()); + assertEquals("work email", id.getComment()); + } } From 44738766e5fa5da50ba883ecd5c347061fc4387c Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 22 Dec 2022 15:19:42 +0100 Subject: [PATCH 198/763] Add comments to regexes --- .../src/main/java/org/pgpainless/key/util/UserId.java | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/util/UserId.java b/pgpainless-core/src/main/java/org/pgpainless/key/util/UserId.java index 1a2ea265..f59cd36f 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/key/util/UserId.java +++ b/pgpainless-core/src/main/java/org/pgpainless/key/util/UserId.java @@ -12,12 +12,20 @@ import javax.annotation.Nullable; public final class UserId implements CharSequence { + // Email regex: https://emailregex.com/ + // switched "a-z0-9" to "\p{L}\u0900-\u097F0-9" for better support for international characters + // \\p{L} = Unicode Letters + // \u0900-\u097F = Hindi Letters private static final Pattern emailPattern = Pattern.compile("(?:[\\p{L}\\u0900-\\u097F0-9!#\\$%&'*+/=?^_`{|}~-]+(?:\\.[\\p{L}\\u0900-\\u097F0-9!#\\$%&'*+/=?^_`{|}~-]+)*|\"(?:[\\x01-\\x08\\x0b\\x0c\\x0e-" + "\\x1f\\x21\\x23-\\x5b\\x5d-\\x7f]|\\\\[\\x01-\\x09\\x0b\\x0c\\x0e-\\x7f])*\")@(?:(?:[\\p{L}\\u0900-\\u097F0-9](?:[\\p{L}\\u0900-\\u097F0-9" + "-]*[\\p{L}\\u0900-\\u097F0-9])?\\.)+[\\p{L}\\u0900-\\u097F0-9](?:[\\p{L}\\u0900-\\u097F0-9-]*[\\p{L}\\u0900-\\u097F0-9])?|\\[(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)" + "\\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?|[$\\p{L}\\u0900-\\u097F0-9-]*[\\p{L}\\u0900-\\u097F0-9]:(?:[\\x01-\\x08\\x0b\\x0c\\x0e-\\x1f" + "\\x21-\\x5a\\x53-\\x7f]|\\\\[\\x01-\\x09\\x0b\\x0c\\x0e-\\x7f])+)])"); + // User-ID Regex + // "Firstname Lastname (Comment) " + // All groups are optional + // https://www.rfc-editor.org/rfc/rfc5322#page-16 private static final Pattern nameAddrPattern = Pattern.compile("^((?.+?)\\s)?(\\((?.+?)\\)\\s)?(<(?.+?)>)?$"); public static final class Builder { From b5128be6fb65e7fb791e8e8e3debae49ec644c90 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 22 Dec 2022 15:23:00 +0100 Subject: [PATCH 199/763] Update changelog --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4bafa039..4493fe1a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,9 @@ SPDX-License-Identifier: CC0-1.0 # PGPainless Changelog +## 1.4.1 +- Add `UserId.parse()` method to parse user-ids into their components + ## 1.4.0 - `sop generate-key`: Add support for keys without user-ids - `sop inline-sign --as=clearsigned`: Make signature in TEXT mode From 35c62663e95c0f444531cc2bf20854d6079792ba Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 22 Dec 2022 15:30:11 +0100 Subject: [PATCH 200/763] Fix javadoc --- .../src/main/java/org/pgpainless/key/util/UserId.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/util/UserId.java b/pgpainless-core/src/main/java/org/pgpainless/key/util/UserId.java index f59cd36f..03a50321 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/key/util/UserId.java +++ b/pgpainless-core/src/main/java/org/pgpainless/key/util/UserId.java @@ -94,7 +94,7 @@ public final class UserId implements CharSequence { *
    *
  • Local domains without TLDs (
    user@localdomain1
    )
  • *
  • " "@example.org
    (spaces between the quotes)
  • - *
  • "very.(),:;<>[]\".VERY.\"very@\\ \"very\".unusual"@strange.example.com
  • + *
  • "very.(),:;<>[]\".VERY.\"very@\\ \"very\".unusual"@strange.example.com
  • *
* * @see RFC5322 §3.4. Address Specification From 77c476dff2c0015cdac8be63d708e7081e136941 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 22 Dec 2022 15:31:32 +0100 Subject: [PATCH 201/763] PGPainless 1.4.1 --- README.md | 2 +- pgpainless-sop/README.md | 4 ++-- version.gradle | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 6dbceacd..49320f94 100644 --- a/README.md +++ b/README.md @@ -191,7 +191,7 @@ repositories { } dependencies { - implementation 'org.pgpainless:pgpainless-core:1.4.0' + implementation 'org.pgpainless:pgpainless-core:1.4.1' } ``` diff --git a/pgpainless-sop/README.md b/pgpainless-sop/README.md index e7e612de..c1b2a988 100644 --- a/pgpainless-sop/README.md +++ b/pgpainless-sop/README.md @@ -23,7 +23,7 @@ To start using pgpainless-sop in your code, include the following lines in your ... dependencies { ... - implementation "org.pgpainless:pgpainless-sop:1.4.0" + implementation "org.pgpainless:pgpainless-sop:1.4.1" ... } @@ -34,7 +34,7 @@ dependencies { org.pgpainless pgpainless-sop - 1.4.0 + 1.4.1 ... diff --git a/version.gradle b/version.gradle index 65038afb..76939ca7 100644 --- a/version.gradle +++ b/version.gradle @@ -5,7 +5,7 @@ allprojects { ext { shortVersion = '1.4.1' - isSnapshot = true + isSnapshot = false pgpainlessMinAndroidSdk = 10 javaSourceCompatibility = 1.8 bouncyCastleVersion = '1.72' From 7be12b0aaaca1bb5fcdecf55ef7e947cdc385986 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 22 Dec 2022 15:33:24 +0100 Subject: [PATCH 202/763] PGPainless 1.4.2-SNAPSHOT --- version.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/version.gradle b/version.gradle index 76939ca7..c6f7104e 100644 --- a/version.gradle +++ b/version.gradle @@ -4,8 +4,8 @@ allprojects { ext { - shortVersion = '1.4.1' - isSnapshot = false + shortVersion = '1.4.2' + isSnapshot = true pgpainlessMinAndroidSdk = 10 javaSourceCompatibility = 1.8 bouncyCastleVersion = '1.72' From 94d9efa1e7c123445b24754fec13631672f6d4fa Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 2 Jan 2023 13:12:14 +0100 Subject: [PATCH 203/763] OpenPgpMessageInputStream: Ignore non-integrity-protected data if configured --- .../decryption_verification/OpenPgpMessageInputStream.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java index c26c949b..bb5d67b3 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java @@ -409,8 +409,10 @@ public class OpenPgpMessageInputStream extends DecryptionStream { PGPEncryptedDataList encDataList = packetInputStream.readEncryptedDataList(); if (!encDataList.isIntegrityProtected()) { - LOGGER.debug("Symmetrically Encrypted Data Packet is not integrity-protected and is therefore rejected."); - throw new MessageNotIntegrityProtectedException(); + LOGGER.warn("Symmetrically Encrypted Data Packet is not integrity-protected."); + if (!options.isIgnoreMDCErrors()) { + throw new MessageNotIntegrityProtectedException(); + } } SortedESKs esks = new SortedESKs(encDataList); From 00b593823a2bf7fce7b897c530f60032c2d16d7c Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 2 Jan 2023 13:18:18 +0100 Subject: [PATCH 204/763] Modify SED test to test successful decryption of SED packet --- .../ModificationDetectionTests.java | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/ModificationDetectionTests.java b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/ModificationDetectionTests.java index 9ecaa38a..59021f95 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/ModificationDetectionTests.java +++ b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/ModificationDetectionTests.java @@ -4,6 +4,7 @@ package org.pgpainless.decryption_verification; +import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; import java.io.ByteArrayInputStream; @@ -374,7 +375,7 @@ public class ModificationDetectionTests { @TestTemplate @ExtendWith(TestAllImplementations.class) - public void decryptMessageWithSEDPacket() throws IOException { + public void decryptMessageWithSEDPacket() throws IOException, PGPException { Passphrase passphrase = Passphrase.fromPassword("flowcrypt compatibility tests"); String key = "-----BEGIN PGP PRIVATE KEY BLOCK-----\r\n" + "Version: FlowCrypt 6.9.1 Gmail Encryption\r\n" + @@ -536,6 +537,21 @@ public class ModificationDetectionTests { .withOptions(new ConsumerOptions().addDecryptionKey(secretKeyRing, SecretKeyRingProtector.unlockAnyKeyWith(passphrase))) ); + + DecryptionStream decryptionStream = PGPainless.decryptAndOrVerify() + .onInputStream(new ByteArrayInputStream(ciphertext.getBytes(StandardCharsets.UTF_8))) + .withOptions(ConsumerOptions.get().addDecryptionKey(secretKeyRing, + SecretKeyRingProtector.unlockAnyKeyWith(passphrase)) + .setIgnoreMDCErrors(true)); + ByteArrayOutputStream plaintext = new ByteArrayOutputStream(); + Streams.pipeAll(decryptionStream, plaintext); + decryptionStream.close(); + + assertEquals("As stated in subject\r\n" + + "\r\n" + + "Shall not decrypt automatically\r\n" + + "\r\n" + + "Has to show a warning\r\n", plaintext.toString()); } private PGPSecretKeyRingCollection getDecryptionKey() throws IOException { From b36494ecd439546fbf4b4c34c4a63649b396ff7a Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 2 Jan 2023 13:53:12 +0100 Subject: [PATCH 205/763] Update changelog --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4493fe1a..9a35d4d5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,9 @@ SPDX-License-Identifier: CC0-1.0 # PGPainless Changelog +## 1.4.2-SNAPSHOT +- Properly decrypt messages without MDC packets when `ConsumerOptions.setIgnoreMDCErrors(true)` is set + ## 1.4.1 - Add `UserId.parse()` method to parse user-ids into their components From 507b36468b299173da06f4a21583763f2697541d Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 2 Jan 2023 14:06:19 +0100 Subject: [PATCH 206/763] Update docs on UserId parsing --- docs/source/pgpainless-core/userids.md | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/docs/source/pgpainless-core/userids.md b/docs/source/pgpainless-core/userids.md index 78126828..d2b5730e 100644 --- a/docs/source/pgpainless-core/userids.md +++ b/docs/source/pgpainless-core/userids.md @@ -29,4 +29,19 @@ UserId full = UserId.newBuilder() .withComment("Work Address") .build(); assertEquals("Peter Pattern (Work Address) ", full.toString()); -``` \ No newline at end of file +``` + +If you have a User-ID in form of a string (e.g. because a user provided it via a text field), +you can parse it into its components like this: + +```java +String string = "John Doe "; +UserId userId = UserId.parse(string); + +// Now you can access the different components +assertEquals("John Doe", userId.getName()); +assertEquals("john@doe.corp", userId.getEmail()); +assertNull(userId.getComment()); +``` + +The method `UserId.parse(String string)` will throw an `IllegalArgumentException` if the User-ID is malformed. From 1452af71fd885b65f781b4104212c96d5f0e7cea Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 2 Jan 2023 15:23:29 +0100 Subject: [PATCH 207/763] Add more docs for Policy related configuration --- docs/source/pgpainless-core/quickstart.md | 162 ++++++++++++++++++++++ 1 file changed, 162 insertions(+) diff --git a/docs/source/pgpainless-core/quickstart.md b/docs/source/pgpainless-core/quickstart.md index d61a881c..5382923f 100644 --- a/docs/source/pgpainless-core/quickstart.md +++ b/docs/source/pgpainless-core/quickstart.md @@ -325,4 +325,166 @@ verificationStream.close(); // finish verification MessageMetadata result = verificationStream.getMetadata(); // get metadata of signed message assertTrue(result.isVerifiedSignedBy(certificate)); // check if message was in fact signed +``` + +### Legacy Compatibility +Out of the box, PGPainless is configured to use secure defaults and perform checks for recommended +security features. This means that for example messages generated using older OpenPGP +implementations which do not follow those best practices might fail to decrypt/verify. + +It is however possible to circumvent certain security checks to allow processing of such messages. + +:::{note} +It is not recommended to disable security checks, as that might enable certain attacks on the OpenPGP protocol. +::: + +#### Missing / broken MDC (modification detection code) +RFC4880 has two different types of encrypted data packets. The *Symmetrically Encrypted Data* packet (SED) and the *Symmetrically Encrypted Integrity-Protected Data* packet. +The latter has an added MDC packet which prevents modifications to the ciphertext. + +While implementations are highly encouraged to only use the latter package type, some older implementations still generate +encrypted data packets which are not integrity protected. + +To allow PGPainless to decrypt such messages, you need to set a flag in the `ConsumerOptions` object: +```java +ConsumerOptions options = ConsumerOptions.get() + .setIgnoreMDCErrors(true) // <- + .setDecryptionKey(secretKey) + ... + +DecryptionStream decryptionStream = PGPainless.decryptAndOrVerify() + .onInputStream(ciphertextIn) + .withOptions(options); +... +``` + +:::{note} +It is highly advised to only set this flag if you know what you are doing. +It might also be a good idea to try decrypting a message without the flag set first and only re-try +decryption with the flag set in case of a `MessageNotIntegrityProtectedException` (don't forget to rewind the ciphertextInputStream). +::: + +#### Weak keys and broken algorithms +Some users might cling on to older keys using weak algorithms / small key sizes. +PGPainless refuses to encrypt to weak certificates and sign with weak keys. +By default, PGPainless follows the recommendations for acceptable key sizes of [the German BSI in 2021](https://www.bsi.bund.de/SharedDocs/Downloads/EN/BSI/Publications/TechGuidelines/TG02102/BSI-TR-02102-1.pdf). +It can however be configured to accept older key material / algorithms too. + +Minimal key lengths can be configured by changing PGPainless' policy: +```java +Map algorithms = new HashMap<>(); +// put all acceptable algorithms and their minimal key length +algorithms.put(PublicKeyAlgorithm.RSA_GENERAL, 1024); +algorithms.put(PublicKeyAlgorithm.ECDSA, 100); +... +Policy.PublicKeyAlgorithmPolicy pkPolicy = + new Policy.PublicKeyAlgorithmPolicy(algorithms); +// set the custom algorithm policy +PGPainless.getPolicy().setPublicKeyAlgorithmPolicy(); +``` + +Since OpenPGP uses a hybrid encryption scheme of asymmetric and symmetric encryption algorithms, +it also comes with a policy for symmetric encryption algorithms. +This list can be modified to allow for weaker algorithms like follows: +```java +// default fallback algorithm for message encryption +SymmetricKeyAlgorithm fallbackAlgorithm = SymmetricKeyAlgorithm.AES_256; +// acceptable algorithms +List algorithms = new ArrayList<>(); +algorithms.add(SymmetricKeyAlgorithm.AES_256); +algorithms.add(SymmetricKeyAlgorithm.AES_192); +algorithms.add(SymmetricKeyAlgorithm.AES_128); +algorithms.add(SymmetricKeyAlgorithm.TWOFISH); +algorithms.add(SymmetricKeyAlgorithm.BLOWFISH); +... +Policy.SymmetricKeyAlgorithmPolicy skPolicy = + new SymmtricKeyAlgorithmPolicy(fallbackAlgorithm, algorithms); +// set the custom algorithm policy +// algorithm policy applicable when decrypting messages created by legacy senders: +PGPainless.getPolicy() + .setSymmetricKeyDecryptionAlgorithmPolicy(skPolicy); +// algorithm policy applicable when generating messages for legacy recipients: +PGPainless.getPolicy() + .setSymmetricKeyEncryptionAlgorithmPolicy(skPolicy); +``` + +Hash algorithms are used in OpenPGP to create signatures. +Since signature verification is an integral part of the OpenPGP protocol, PGPainless comes +with multiple policies for acceptable hash algorithms, depending on the use-case. +Revocation signatures are critical, so you might want to handle revocation signatures differently from normal signatures. + +By default, PGPainless uses a smart hash algorithm policy for both use-cases, which takes into consideration +not only the hash algorithm itself, but also the creation date of the signature. +That way, signatures using SHA-1 are acceptable if they were created before February 2013, but are rejected if their +creation date is after that point in time. + +A custom hash algorithm policy can be set like this: +```java +HashAlgorithm fallbackAlgorithm = HashAlgorithm.SHA512; +Map algorithms = new HashMap<>(); +// Accept MD5 on signatures made before 1997-02-01 +algorithms.put(HashAlgorithm.MD5, + DateUtil.parseUTCDate("1997-02-01 00:00:00 UTC")); +// Accept SHA-1, regardless of signature creation time +algorithms.put(HashAlgorithm.SHA1, null); +... +Policy.HashAlgorithmPolicy hPolicy = + new Policy.HashAlgorithmPolicy(fallbackAlgorithm, algorithms); +// set policy for revocation signatures +PGPainless.getPolicy() + .setRevocationSignatureHashAlgorithmPolicy(hPolicy); +// set policy for normal signatures (certifications and document signatures) +PGPainless.getPolicy() + .setSignatureHashAlgorithmPolicy(hPolicy); +``` + +Lastly, PGPainless comes with a policy on acceptable compression algorithms, which currently accepts any +compression algorithm. +A custom compression algorithm policy can be set in a similar way: +```java +CompressionAlgorithm fallback = CompressionAlgorithm.ZIP; +List algorithms = new ArrayList<>(); +algorithms.add(CompressionAlgorith.ZIP); +algorithms.add(CompressionAlgorithm.BZIP2); +... +Policy.CompressionAlgorithmPolicy cPolicy = + new Policy.CompressionAlgorithmPolicy(fallback, algorithms); +PGPainless.getPolicy() + .setCompressionAlgorithmPolicy(cPolicy); +``` + +To prevent a class of attacks described in the [paper](https://www.kopenpgp.com/#paper) +"Victory by KO: Attacking OpenPGP Using Key Overwriting", +PGPainless offers the option to validate private key material each time before using it, +to make sure that an attacker didn't tamper with the corresponding public key parameters. + +These checks are disabled by default, but they can be enabled as follows: +```java +PGPainless.getPolicy() + .setEnableKeyParameterValidation(true); +``` + +:::{note} +Validation checks against KOpenPGP attacks are disabled by default, since they are very costly +and only make sense in certain scenarios. +Please read and understand the paper to decide, if enabling the checks makes sense for your use-case. +::: + + +### Known Notations +In OpenPGP, signatures can contain [notation subpackets](https://www.rfc-editor.org/rfc/rfc4880#section-5.2.3.16). +A notation can give meaning to a signature, or add additional contextual information. +Signature subpackets can be marked as critical, meaning an implementation that does not know about +a certain subpacket MUST reject the signature. +The same is true for critical notations. + +For that reason, PGPainless comes with a `NotationRegistry` class which can be used to register known notations, +such that a signature containing a critical notation of a certain value is not rejected. +To register a known notation, you can do the following: + +```java +NotationRegistry registry = PGPainless.getPolicy() + .getNotationRegistry(); + +registry.addKnownNotation("sample@example.com"); ``` \ No newline at end of file From 51c7bc932b73a7c55571ef3907cd7b644d588ddd Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 2 Jan 2023 15:35:55 +0100 Subject: [PATCH 208/763] Bump gradlew to 7.5 --- gradle/wrapper/gradle-wrapper.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index a488f210..8049c684 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-6.4-rc-1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists From abf723cc6c94a492993835a2a01d78afe789cb06 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 4 Jan 2023 18:27:14 +0100 Subject: [PATCH 209/763] Add note about UserId.parse().toString() not guaranteing identity --- .../src/main/java/org/pgpainless/key/util/UserId.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/util/UserId.java b/pgpainless-core/src/main/java/org/pgpainless/key/util/UserId.java index 03a50321..427f9060 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/key/util/UserId.java +++ b/pgpainless-core/src/main/java/org/pgpainless/key/util/UserId.java @@ -96,6 +96,8 @@ public final class UserId implements CharSequence { *
  • " "@example.org
    (spaces between the quotes)
  • *
  • "very.(),:;<>[]\".VERY.\"very@\\ \"very\".unusual"@strange.example.com
  • * + * Note: This method does not guarantee that
    string.equals(UserId.parse(string).toString())
    is true. + * For example,
    UserId.parse("alice@pgpainless.org").toString()
    wraps the mail address in angled brackets. * * @see RFC5322 §3.4. Address Specification * @param string user-id From 41cc71c274a3560019010de300a56a035aabb6d4 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 4 Jan 2023 18:50:10 +0100 Subject: [PATCH 210/763] Add missing javadoc to ConsumerOptions --- .../ConsumerOptions.java | 99 ++++++++++++++++--- 1 file changed, 87 insertions(+), 12 deletions(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/ConsumerOptions.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/ConsumerOptions.java index f7b3c020..33404c48 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/ConsumerOptions.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/ConsumerOptions.java @@ -42,7 +42,6 @@ import pgp.certificate_store.exception.BadDataException; */ public class ConsumerOptions { - private boolean ignoreMDCErrors = false; private boolean forceNonOpenPgpData = false; @@ -55,7 +54,8 @@ public class ConsumerOptions { // Session key for decryption without passphrase/key private SessionKey sessionKey = null; - private final Map customPublicKeyDataDecryptorFactories = new HashMap<>(); + private final Map customPublicKeyDataDecryptorFactories = + new HashMap<>(); private final Map decryptionKeys = new HashMap<>(); private final Set decryptionPassphrases = new HashSet<>(); @@ -135,16 +135,38 @@ public class ConsumerOptions { return this; } + /** + * Pass in a {@link PGPCertificateStore} from which certificates can be sourced for signature verification. + * + * @param certificateStore certificate store + * @return options + */ public ConsumerOptions addVerificationCerts(PGPCertificateStore certificateStore) { this.certificates.addStore(certificateStore); return this; } - public ConsumerOptions addVerificationOfDetachedSignatures(InputStream signatureInputStream) throws IOException, PGPException { + /** + * Add some detached signatures from the given {@link InputStream} for verification. + * + * @param signatureInputStream input stream of detached signatures + * @return options + * + * @throws IOException in case of an IO error + * @throws PGPException in case of an OpenPGP error + */ + public ConsumerOptions addVerificationOfDetachedSignatures(InputStream signatureInputStream) + throws IOException, PGPException { List signatures = SignatureUtils.readSignatures(signatureInputStream); return addVerificationOfDetachedSignatures(signatures); } + /** + * Add some detached signatures for verification. + * + * @param detachedSignatures detached signatures + * @return options + */ public ConsumerOptions addVerificationOfDetachedSignatures(List detachedSignatures) { for (PGPSignature signature : detachedSignatures) { addVerificationOfDetachedSignature(signature); @@ -211,14 +233,15 @@ public class ConsumerOptions { } /** - * Add a key for message decryption. If the key is encrypted, the {@link SecretKeyRingProtector} is used to decrypt it - * when needed. + * Add a key for message decryption. If the key is encrypted, the {@link SecretKeyRingProtector} + * is used to decrypt it when needed. * * @param key key * @param keyRingProtector protector for the secret key * @return options */ - public ConsumerOptions addDecryptionKey(@Nonnull PGPSecretKeyRing key, @Nonnull SecretKeyRingProtector keyRingProtector) { + public ConsumerOptions addDecryptionKey(@Nonnull PGPSecretKeyRing key, + @Nonnull SecretKeyRingProtector keyRingProtector) { decryptionKeys.put(key, keyRingProtector); return this; } @@ -230,7 +253,8 @@ public class ConsumerOptions { * @param keyRingProtector protector for encrypted secret keys * @return options */ - public ConsumerOptions addDecryptionKeys(@Nonnull PGPSecretKeyRingCollection keys, @Nonnull SecretKeyRingProtector keyRingProtector) { + public ConsumerOptions addDecryptionKeys(@Nonnull PGPSecretKeyRingCollection keys, + @Nonnull SecretKeyRingProtector keyRingProtector) { for (PGPSecretKeyRing key : keys) { addDecryptionKey(key, keyRingProtector); } @@ -264,14 +288,31 @@ public class ConsumerOptions { return this; } + /** + * Return the custom {@link PublicKeyDataDecryptorFactory PublicKeyDataDecryptorFactories} that were + * set by the user. + * These factories can be used to decrypt session keys using a custom logic. + * + * @return custom decryptor factories + */ Map getCustomDecryptorFactories() { return new HashMap<>(customPublicKeyDataDecryptorFactories); } + /** + * Return the set of available decryption keys. + * + * @return decryption keys + */ public @Nonnull Set getDecryptionKeys() { return Collections.unmodifiableSet(decryptionKeys.keySet()); } + /** + * Return the set of available message decryption passphrases. + * + * @return decryption passphrases + */ public @Nonnull Set getDecryptionPassphrases() { return Collections.unmodifiableSet(decryptionPassphrases); } @@ -287,18 +328,40 @@ public class ConsumerOptions { return certificates.getExplicitCertificates(); } + /** + * Return an object holding available certificates for signature verification. + * + * @return certificate source + */ public @Nonnull CertificateSource getCertificateSource() { return certificates; } + /** + * Return the callback that gets called when a certificate for signature verification is missing. + * This method might return
    null
    if the users hasn't set a callback. + * + * @return missing public key callback + */ public @Nullable MissingPublicKeyCallback getMissingCertificateCallback() { return missingCertificateCallback; } + /** + * Return the {@link SecretKeyRingProtector} for the given {@link PGPSecretKeyRing}. + * + * @param decryptionKeyRing secret key + * @return protector for that particular secret key + */ public @Nonnull SecretKeyRingProtector getSecretKeyProtector(PGPSecretKeyRing decryptionKeyRing) { return decryptionKeys.get(decryptionKeyRing); } + /** + * Return the set of detached signatures the user provided. + * + * @return detached signatures + */ public @Nonnull Set getDetachedSignatures() { return Collections.unmodifiableSet(detachedSignatures); } @@ -307,12 +370,14 @@ public class ConsumerOptions { * By default, PGPainless will require encrypted messages to make use of SEIP data packets. * Those are Symmetrically Encrypted Integrity Protected Data packets. * Symmetrically Encrypted Data Packets without integrity protection are rejected by default. - * Furthermore, PGPainless will throw an exception if verification of the MDC error detection code of the SEIP packet - * fails. + * Furthermore, PGPainless will throw an exception if verification of the MDC error detection + * code of the SEIP packet fails. * - * Failure of MDC verification indicates a tampered ciphertext, which might be the cause of an attack or data corruption. + * Failure of MDC verification indicates a tampered ciphertext, which might be the cause of an + * attack or data corruption. * - * This method can be used to ignore MDC errors and allow PGPainless to consume encrypted data without integrity protection. + * This method can be used to ignore MDC errors and allow PGPainless to consume encrypted data + * without integrity protection. * If the flag
    ignoreMDCErrors
    is set to true, PGPainless will *
      *
    • not throw exceptions for SEIP packets with tampered ciphertext
    • @@ -323,7 +388,8 @@ public class ConsumerOptions { * * It will however still throw an exception if it encounters a SEIP packet with missing or truncated MDC * - * @see Sym. Encrypted Integrity Protected Data Packet + * @see + * Sym. Encrypted Integrity Protected Data Packet * @param ignoreMDCErrors true if MDC errors or missing MDCs shall be ignored, false otherwise. * @return options */ @@ -353,6 +419,11 @@ public class ConsumerOptions { return this; } + /** + * Return true, if the ciphertext should be handled as binary non-OpenPGP data. + * + * @return true if non-OpenPGP data is forced + */ boolean isForceNonOpenPgpData() { return forceNonOpenPgpData; } @@ -407,6 +478,10 @@ public class ConsumerOptions { return multiPassStrategy; } + /** + * Source for OpenPGP certificates. + * When verifying signatures on a message, this object holds available signer certificates. + */ public static class CertificateSource { private List stores = new ArrayList<>(); From 980daeca31f5f1554f86f34eeb67164a7d73fc2b Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 4 Jan 2023 18:55:57 +0100 Subject: [PATCH 211/763] Add missing javadoc to CustomPublicKeyDataDecryptorFactory --- .../CustomPublicKeyDataDecryptorFactory.java | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/CustomPublicKeyDataDecryptorFactory.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/CustomPublicKeyDataDecryptorFactory.java index 37dc10a9..91902cc7 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/CustomPublicKeyDataDecryptorFactory.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/CustomPublicKeyDataDecryptorFactory.java @@ -7,8 +7,21 @@ package org.pgpainless.decryption_verification; import org.bouncycastle.openpgp.operator.PublicKeyDataDecryptorFactory; import org.pgpainless.key.SubkeyIdentifier; +/** + * Custom {@link PublicKeyDataDecryptorFactory} which can enable customized implementations of message decryption + * using public keys. + * This class can for example be used to implement message encryption using hardware tokens like smartcards or + * TPMs. + * @see ConsumerOptions#addCustomDecryptorFactory(CustomPublicKeyDataDecryptorFactory) + */ public interface CustomPublicKeyDataDecryptorFactory extends PublicKeyDataDecryptorFactory { + /** + * Return the {@link SubkeyIdentifier} for which this particular {@link CustomPublicKeyDataDecryptorFactory} + * is intended. + * + * @return subkey identifier + */ SubkeyIdentifier getSubkeyIdentifier(); } From 3b2d0795f7e62462a17a0d9651d8eb9adcfa11bf Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 15 Dec 2022 12:25:35 +0100 Subject: [PATCH 212/763] Fix NPE when sop generate-key --with-key-password is used with multiple uids Fixes #351 --- CHANGELOG.md | 2 ++ .../src/main/java/org/pgpainless/sop/GenerateKeyImpl.java | 3 ++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9a35d4d5..d5be2adc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,8 @@ SPDX-License-Identifier: CC0-1.0 ## 1.4.2-SNAPSHOT - Properly decrypt messages without MDC packets when `ConsumerOptions.setIgnoreMDCErrors(true)` is set +- Fix crash in `sop generate-key --with-key-password` when more than one user-id is given + ## 1.4.1 - Add `UserId.parse()` method to parse user-ids into their components diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/GenerateKeyImpl.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/GenerateKeyImpl.java index c75087c8..70561693 100644 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/GenerateKeyImpl.java +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/GenerateKeyImpl.java @@ -51,6 +51,7 @@ public class GenerateKeyImpl implements GenerateKey { @Override public Ready generate() throws SOPGPException.MissingArg, SOPGPException.UnsupportedAsymmetricAlgo { Iterator userIdIterator = userIds.iterator(); + Passphrase passphraseCopy = new Passphrase(passphrase.getChars()); // generateKeyRing clears the original passphrase PGPSecretKeyRing key; try { String primaryUserId = userIdIterator.hasNext() ? userIdIterator.next() : null; @@ -61,7 +62,7 @@ public class GenerateKeyImpl implements GenerateKey { SecretKeyRingEditorInterface editor = PGPainless.modifyKeyRing(key); while (userIdIterator.hasNext()) { - editor.addUserId(userIdIterator.next(), SecretKeyRingProtector.unprotectedKeys()); + editor.addUserId(userIdIterator.next(), SecretKeyRingProtector.unlockAnyKeyWith(passphraseCopy)); } key = editor.done(); From ab6b6ca2e74fc192781d74db183ae47fab38e1ec Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 15 Dec 2022 12:30:30 +0100 Subject: [PATCH 213/763] Add regression test for #351 --- .../org/pgpainless/sop/GenerateKeyTest.java | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/pgpainless-sop/src/test/java/org/pgpainless/sop/GenerateKeyTest.java b/pgpainless-sop/src/test/java/org/pgpainless/sop/GenerateKeyTest.java index a71eda12..3a6e4476 100644 --- a/pgpainless-sop/src/test/java/org/pgpainless/sop/GenerateKeyTest.java +++ b/pgpainless-sop/src/test/java/org/pgpainless/sop/GenerateKeyTest.java @@ -6,15 +6,20 @@ package org.pgpainless.sop; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertTrue; import java.io.IOException; +import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.PGPSecretKey; import org.bouncycastle.openpgp.PGPSecretKeyRing; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.pgpainless.PGPainless; import org.pgpainless.key.info.KeyRingInfo; +import org.pgpainless.key.protection.UnlockSecretKey; +import org.pgpainless.util.Passphrase; import sop.SOP; public class GenerateKeyTest { @@ -67,4 +72,24 @@ public class GenerateKeyTest { assertFalse(new String(bytes).startsWith("-----BEGIN PGP PRIVATE KEY BLOCK-----")); } + + @Test + public void protectedMultiUserIdKey() throws IOException, PGPException { + byte[] bytes = sop.generateKey() + .userId("Alice") + .userId("Bob") + .withKeyPassword("sw0rdf1sh") + .generate() + .getBytes(); + + PGPSecretKeyRing secretKey = PGPainless.readKeyRing().secretKeyRing(bytes); + KeyRingInfo info = PGPainless.inspectKeyRing(secretKey); + + assertTrue(info.getUserIds().contains("Alice")); + assertTrue(info.getUserIds().contains("Bob")); + + for (PGPSecretKey key : secretKey) { + assertNotNull(UnlockSecretKey.unlockSecretKey(key, Passphrase.fromPassword("sw0rdf1sh"))); + } + } } From 7a2c9d864c2ecb9db1025dfe23df3872a6b6881e Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Fri, 13 Jan 2023 17:52:59 +0100 Subject: [PATCH 214/763] Add javadoc to DecryptionBuilder --- .../decryption_verification/DecryptionBuilder.java | 5 +++++ 1 file changed, 5 insertions(+) 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 0baf4124..96b4ad60 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 @@ -10,6 +10,11 @@ import javax.annotation.Nonnull; import org.bouncycastle.openpgp.PGPException; +/** + * Builder class that takes an {@link InputStream} of ciphertext (or plaintext signed data) + * and combines it with a configured {@link ConsumerOptions} object to form a {@link DecryptionStream} which + * can be used to decrypt an OpenPGP message or verify signatures. + */ public class DecryptionBuilder implements DecryptionBuilderInterface { @Override From 8cb773841b0c7881fa3e878dda424e746f565ab0 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Fri, 13 Jan 2023 19:18:02 +0100 Subject: [PATCH 215/763] Revert certificate-store integration Integration of certificate-store and pgpainless-cert-d makes packaging complicated. Alternatively, users can simply integrate the certificate-store with PGPainless themselves. --- pgpainless-core/build.gradle | 4 - .../ConsumerOptions.java | 40 --- .../encryption_signing/EncryptionOptions.java | 29 --- .../EncryptWithKeyFromKeyStoreTest.java | 230 ------------------ version.gradle | 2 - 5 files changed, 305 deletions(-) delete mode 100644 pgpainless-core/src/test/java/org/pgpainless/encryption_signing/EncryptWithKeyFromKeyStoreTest.java diff --git a/pgpainless-core/build.gradle b/pgpainless-core/build.gradle index 84b4273f..3c73121f 100644 --- a/pgpainless-core/build.gradle +++ b/pgpainless-core/build.gradle @@ -24,10 +24,6 @@ dependencies { api "org.bouncycastle:bcpg-jdk15to18:$bouncyPgVersion" // api(files("../libs/bcpg-jdk18on-1.70.jar")) - // certificate store - api "org.pgpainless:pgp-certificate-store:$pgpCertDJavaVersion" - testImplementation "org.pgpainless:pgpainless-cert-d:$pgpainlessCertDVersion" - // @Nullable, @Nonnull annotations implementation "com.google.code.findbugs:jsr305:3.0.2" } diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/ConsumerOptions.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/ConsumerOptions.java index 33404c48..d0d9230b 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/ConsumerOptions.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/ConsumerOptions.java @@ -6,12 +6,10 @@ package org.pgpainless.decryption_verification; import java.io.IOException; import java.io.InputStream; -import java.util.ArrayList; import java.util.Collections; import java.util.Date; import java.util.HashMap; import java.util.HashSet; -import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; @@ -25,7 +23,6 @@ import org.bouncycastle.openpgp.PGPSecretKeyRing; import org.bouncycastle.openpgp.PGPSecretKeyRingCollection; import org.bouncycastle.openpgp.PGPSignature; import org.bouncycastle.openpgp.operator.PublicKeyDataDecryptorFactory; -import org.pgpainless.PGPainless; import org.pgpainless.decryption_verification.cleartext_signatures.InMemoryMultiPassStrategy; import org.pgpainless.decryption_verification.cleartext_signatures.MultiPassStrategy; import org.pgpainless.key.SubkeyIdentifier; @@ -33,9 +30,6 @@ import org.pgpainless.key.protection.SecretKeyRingProtector; import org.pgpainless.signature.SignatureUtils; import org.pgpainless.util.Passphrase; import org.pgpainless.util.SessionKey; -import pgp.certificate_store.PGPCertificateStore; -import pgp.certificate_store.certificate.Certificate; -import pgp.certificate_store.exception.BadDataException; /** * Options for decryption and signature verification. @@ -135,17 +129,6 @@ public class ConsumerOptions { return this; } - /** - * Pass in a {@link PGPCertificateStore} from which certificates can be sourced for signature verification. - * - * @param certificateStore certificate store - * @return options - */ - public ConsumerOptions addVerificationCerts(PGPCertificateStore certificateStore) { - this.certificates.addStore(certificateStore); - return this; - } - /** * Add some detached signatures from the given {@link InputStream} for verification. * @@ -484,18 +467,8 @@ public class ConsumerOptions { */ public static class CertificateSource { - private List stores = new ArrayList<>(); private Set explicitCertificates = new HashSet<>(); - /** - * Add a certificate store as source for verification certificates. - * - * @param certificateStore cert store - */ - public void addStore(PGPCertificateStore certificateStore) { - this.stores.add(certificateStore); - } - /** * Add a certificate as verification cert explicitly. * @@ -529,19 +502,6 @@ public class ConsumerOptions { } } - for (PGPCertificateStore store : stores) { - try { - Iterator certs = store.getCertificatesBySubkeyId(keyId); - if (!certs.hasNext()) { - continue; - } - Certificate cert = certs.next(); - PGPPublicKeyRing publicKey = PGPainless.readKeyRing().publicKeyRing(cert.getInputStream()); - return publicKey; - } catch (IOException | BadDataException e) { - continue; - } - } return null; } } diff --git a/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/EncryptionOptions.java b/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/EncryptionOptions.java index cf3b426a..612dcd50 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/EncryptionOptions.java +++ b/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/EncryptionOptions.java @@ -4,7 +4,6 @@ package org.pgpainless.encryption_signing; -import java.io.IOException; import java.util.Collections; import java.util.Date; import java.util.HashMap; @@ -14,7 +13,6 @@ import java.util.List; import java.util.Map; import java.util.NoSuchElementException; import java.util.Set; - import javax.annotation.Nonnull; import org.bouncycastle.openpgp.PGPPublicKey; @@ -22,7 +20,6 @@ import org.bouncycastle.openpgp.PGPPublicKeyRing; import org.bouncycastle.openpgp.PGPPublicKeyRingCollection; import org.bouncycastle.openpgp.operator.PBEKeyEncryptionMethodGenerator; import org.bouncycastle.openpgp.operator.PGPKeyEncryptionMethodGenerator; -import org.pgpainless.PGPainless; import org.pgpainless.algorithm.EncryptionPurpose; import org.pgpainless.algorithm.SymmetricKeyAlgorithm; import org.pgpainless.exception.KeyException; @@ -32,10 +29,6 @@ import org.pgpainless.key.SubkeyIdentifier; import org.pgpainless.key.info.KeyAccessor; import org.pgpainless.key.info.KeyRingInfo; import org.pgpainless.util.Passphrase; -import pgp.certificate_store.PGPCertificateStore; -import pgp.certificate_store.certificate.Certificate; -import pgp.certificate_store.exception.BadDataException; -import pgp.certificate_store.exception.BadNameException; /** * Options for the encryption process. @@ -241,28 +234,6 @@ public class EncryptionOptions { return this; } - /** - * Add a recipient by providing a {@link PGPCertificateStore} and the {@link OpenPgpFingerprint} of the recipients key. - * If no such certificate is found in the store, a {@link NoSuchElementException is thrown}. - * - * @param certificateStore certificate store - * @param certificateFingerprint fingerprint of the recipient certificate - * @return builder - * @throws BadDataException if the certificate contains bad data - * @throws BadNameException if the fingerprint is not in a recognizable form for the store - * @throws IOException in case of an IO error - * @throws NoSuchElementException if the store does not contain a certificate for the given fingerprint - */ - public EncryptionOptions addRecipient(@Nonnull PGPCertificateStore certificateStore, - @Nonnull OpenPgpFingerprint certificateFingerprint) - throws BadDataException, BadNameException, IOException { - String fingerprint = certificateFingerprint.toString().toLowerCase(); - Certificate certificateRecord = certificateStore.getCertificate(fingerprint); - PGPPublicKeyRing recipientCertificate = PGPainless.readKeyRing() - .publicKeyRing(certificateRecord.getInputStream()); - return addRecipient(recipientCertificate); - } - private void addRecipientKey(PGPPublicKeyRing keyRing, PGPPublicKey key) { encryptionKeys.add(new SubkeyIdentifier(keyRing, key.getKeyID())); PGPKeyEncryptionMethodGenerator encryptionMethod = ImplementationFactory diff --git a/pgpainless-core/src/test/java/org/pgpainless/encryption_signing/EncryptWithKeyFromKeyStoreTest.java b/pgpainless-core/src/test/java/org/pgpainless/encryption_signing/EncryptWithKeyFromKeyStoreTest.java deleted file mode 100644 index 48f1bbd7..00000000 --- a/pgpainless-core/src/test/java/org/pgpainless/encryption_signing/EncryptWithKeyFromKeyStoreTest.java +++ /dev/null @@ -1,230 +0,0 @@ -// SPDX-FileCopyrightText: 2022 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.encryption_signing; - -import org.bouncycastle.openpgp.PGPException; -import org.bouncycastle.openpgp.PGPPublicKeyRing; -import org.bouncycastle.openpgp.PGPPublicKeyRingCollection; -import org.bouncycastle.openpgp.PGPSecretKeyRing; -import org.bouncycastle.openpgp.PGPSecretKeyRingCollection; -import org.bouncycastle.util.io.Streams; -import org.junit.jupiter.api.Test; -import org.pgpainless.PGPainless; -import org.pgpainless.certificate_store.MergeCallbacks; -import org.pgpainless.certificate_store.PGPainlessCertD; -import org.pgpainless.decryption_verification.ConsumerOptions; -import org.pgpainless.decryption_verification.DecryptionStream; -import org.pgpainless.decryption_verification.OpenPgpMetadata; -import org.pgpainless.key.OpenPgpFingerprint; -import org.pgpainless.key.protection.SecretKeyRingProtector; -import pgp.cert_d.PGPCertificateStoreAdapter; -import pgp.certificate_store.certificate.Certificate; -import pgp.certificate_store.exception.BadDataException; -import pgp.certificate_store.exception.BadNameException; - -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.IOException; - -import static org.junit.jupiter.api.Assertions.assertTrue; - -public class EncryptWithKeyFromKeyStoreTest { - - // Collection of 3 keys (fingerprints below) - private static final String KEY_COLLECTION = "-----BEGIN PGP PRIVATE KEY BLOCK-----\n" + - "Version: BCPG v1.71\n" + - "\n" + - "lFgEYwerQBYJKwYBBAHaRw8BAQdAl3XjFMXQdmhMuFEIbE7IJUP1k+5utUT6IAW3\n" + - "zlWguvQAAQDK7Qh5Q9EAB5cTh2OWsPeydfDqRmnuxlZjlwf4WWQLhRAltBRBIDxh\n" + - "QHBncGFpbmxlc3Mub3JnPoiPBBMWCgBBBQJjB6tBCRBoj2Vso6FpsxYhBNqK9ZX8\n" + - "QfcbxPJmCGiPZWyjoWmzAp4BApsBBRYCAwEABAsJCAcFFQoJCAsCmQEAACEaAP9P\n" + - "49Q/E19vyx2rV8EjQd+XBFnDuYxBjw80ZVC0TaKJNgEAgWsQqcg/ARkG9XGxaE3X\n" + - "IE9tFHh4wpjQhnK1Ta/wJAOcXQRjB6tBEgorBgEEAZdVAQUBAQdATJM1XKfKVF+C\n" + - "B2/xrGU+F89Ir9viOut4sna4aWfvwHoDAQgHAAD/UN84yv5jxKsPgfw/XZCDwoey\n" + - "Y69ompSiBuZjzOWrjegToIh1BBgWCgAdBQJjB6tBAp4BApsMBRYCAwEABAsJCAcF\n" + - "FQoJCAsACgkQaI9lbKOhabP/PAEApov4hYuhIENq26z+w4s3A1gakN+gax54F7+M\n" + - "YSUm16sBAPiuEdpVJOwTk3WMXKyLOYaVU3JstlP2H1ouguvYTt4CnFgEYwerQRYJ\n" + - "KwYBBAHaRw8BAQdA5xpeGHNy9v+QUbl+Rs7Mx0c6D913gksW1eZ4Qeg31B0AAQCx\n" + - "6b3P5lRBAraZstlRupymrt6vF2JpeJB8JOOQ+rdVYBJpiNUEGBYKAH0FAmMHq0EC\n" + - "ngECmwIFFgIDAQAECwkIBwUVCgkIC18gBBkWCgAGBQJjB6tBAAoJENH9GnI3A/RM\n" + - "IVMA/1GU9E+vA8bs0vJVDjp1ri3J4S7u+abwmlivDw8g8XCWAPwKWWfHLgJCsAHk\n" + - "INuDgJdqbNPATFiXxH9FqYnOvWy6DAAKCRBoj2Vso6Fps884AP9D5ZOwuBEXyT/j\n" + - "0G8CWBZ0lT14kRGFucjQi9kZStAuVgEA5cd3eUWofnekd/P6R3UgmvhVOqvxwUUg\n" + - "Y3mEArH7+waUWARjB6tBFgkrBgEEAdpHDwEBB0BCYWjTs0pfBnKYgO0O07djiMSB\n" + - "tUJVpUFo6zrVK92RgAAA/38G6IEK5rJs1OCusmmhHJk1vDu0hbesK7JH7dh75mVY\n" + - "Ep20FEIgPGJAcGdwYWlubGVzcy5vcmc+iI8EExYKAEEFAmMHq0EJEAnsE6FTTHNl\n" + - "FiEE2/L5HBba6IFDHu8cCewToVNMc2UCngECmwEFFgIDAQAECwkIBwUVCgkICwKZ\n" + - "AQAAS7MBAI74uYLK7XR6oCwWYk7C6nwdgu3t478MaEpVHQz/9nEGAQCvJCYqqOd6\n" + - "cAG6fwFaIJ3h99/Y5o2NaiN17S2zOXEZDJxdBGMHq0ESCisGAQQBl1UBBQEBB0BU\n" + - "EjXQCT4xwJryksXsMLaFo43pFTwWaTzduiWgCy2KMgMBCAcAAP9lXlnMYtBfXpgH\n" + - "doUZZk3cvWBOH3awc12V3jZSLtSE8BAJiHUEGBYKAB0FAmMHq0ECngECmwwFFgID\n" + - "AQAECwkIBwUVCgkICwAKCRAJ7BOhU0xzZf5lAQDOgzMhqg3fE8Hg4Hbt4+B0fAD0\n" + - "kp6EJgsKRWT7KbZ0SQD/aVGFv7VRVqiiqOT/YMQKBBwHnq/CGJqxUwUmavBMRAqc\n" + - "WARjB6tBFgkrBgEEAdpHDwEBB0A5kv3bpsnlxs2LrAzeBx4RgtXQNBhGRhzko1to\n" + - "4q+ebQAA/1SU1hvrqd9gNmcc4wff1iwJ1dnqnrbGbO1Yz9rYZjXRE4iI1QQYFgoA\n" + - "fQUCYwerQQKeAQKbAgUWAgMBAAQLCQgHBRUKCQgLXyAEGRYKAAYFAmMHq0EACgkQ\n" + - "pYWdiAVpxGRW4AD+Lade9kJrvcBMSq8EERhYTH6DFka4eMgFB76kH31WmpQA+gOU\n" + - "7kwqKmtyVsXVgCLGMcdTvbZr+73C5m8R7LsdY5kEAAoJEAnsE6FTTHNl7BAA/2v8\n" + - "Wzfmg1OO6IWCohmmNgF4rIDBW8Q9s3+1I/mWlMyjAP9YGR+fnN/YOQrlSG9UiXE5\n" + - "fGwUhaPB0LEGWp0wmmQYA5RYBGMHq0EWCSsGAQQB2kcPAQEHQI8C53+C8crLCQ48\n" + - "OKQa1dEKc8XWQSA6Ckg5j73tOJRLAAD/VRvioGU2M9G6+eKTn68mBVZ8G512HELr\n" + - "apK9M5UFGUMPXLQUQyA8Y0BwZ3BhaW5sZXNzLm9yZz6IjwQTFgoAQQUCYwerQQkQ\n" + - "ommXHYx1l94WIQQp+Mrw86EV1myUgUKiaZcdjHWX3gKeAQKbAQUWAgMBAAQLCQgH\n" + - "BRUKCQgLApkBAAAQ5wEAvahnnRuwY+Y7EPSQG+sqhsdvSTumleYPtEOnHfKctpkA\n" + - "/iaTp4OoUw/RtyWUAk8MLN47CAW5wwhFUbVfZOaS88wMnF0EYwerQRIKKwYBBAGX\n" + - "VQEFAQEHQNz/s68ZGUBfDmMz510cFgHz+mAdC2nXeE4hHKV/HIVsAwEIBwAA/1HB\n" + - "vRl84B8r/PY+5j/X6A+4J08QB/vd5wIHVdkrX+xQELGIdQQYFgoAHQUCYwerQQKe\n" + - "AQKbDAUWAgMBAAQLCQgHBRUKCQgLAAoJEKJplx2MdZfeqzYA/jLtjRmy42MCOxnF\n" + - "3A95WZIDoEohFU0QAeE/yVTLGoDTAP4xhTznleABK7VbD9GJXfD6DkEC749tOsST\n" + - "eYO/GOxKDpxYBGMHq0EWCSsGAQQB2kcPAQEHQFnvyWSgOv4gn3Ch3RY74pRg+7hX\n" + - "OBJAf6ybwvx9t4olAAEAwYG1CL0JozVD1216yrENkP8La132O1MI28kqMsoF6FcP\n" + - "I4jVBBgWCgB9BQJjB6tBAp4BApsCBRYCAwEABAsJCAcFFQoJCAtfIAQZFgoABgUC\n" + - "YwerQQAKCRB8jJGVps/ENgz7AP9ZMENJH+rIKMjynb9WPBlvJ8yJ9dMhzCxcssxg\n" + - "EVZYXAEA5ZsE5xJLQC/cVMGFvqaQ8iPo5jhDZpQJ8RCVlb8XzQwACgkQommXHYx1\n" + - "l96SkgD/f0FYkK4yB8FWuntJ3n0FUfE31wDwpxvvpvP+o3d2GB4BAP9LRKBXMwj4\n" + - "jzJc4ViKmwiNJAPttDQCpYjzJT7LUKAA\n" + - "=EAvh\n" + - "-----END PGP PRIVATE KEY BLOCK-----"; - - // Collection of 3 certificates (fingerprints below) - private static final String CERT_COLLECTION = "-----BEGIN PGP PUBLIC KEY BLOCK-----\n" + - "Version: BCPG v1.71\n" + - "\n" + - "mDMEYwerQBYJKwYBBAHaRw8BAQdAl3XjFMXQdmhMuFEIbE7IJUP1k+5utUT6IAW3\n" + - "zlWguvS0FEEgPGFAcGdwYWlubGVzcy5vcmc+iI8EExYKAEEFAmMHq0EJEGiPZWyj\n" + - "oWmzFiEE2or1lfxB9xvE8mYIaI9lbKOhabMCngECmwEFFgIDAQAECwkIBwUVCgkI\n" + - "CwKZAQAAIRoA/0/j1D8TX2/LHatXwSNB35cEWcO5jEGPDzRlULRNook2AQCBaxCp\n" + - "yD8BGQb1cbFoTdcgT20UeHjCmNCGcrVNr/AkA7g4BGMHq0ESCisGAQQBl1UBBQEB\n" + - "B0BMkzVcp8pUX4IHb/GsZT4Xz0iv2+I663iydrhpZ+/AegMBCAeIdQQYFgoAHQUC\n" + - "YwerQQKeAQKbDAUWAgMBAAQLCQgHBRUKCQgLAAoJEGiPZWyjoWmz/zwBAKaL+IWL\n" + - "oSBDatus/sOLNwNYGpDfoGseeBe/jGElJterAQD4rhHaVSTsE5N1jFysizmGlVNy\n" + - "bLZT9h9aLoLr2E7eArgzBGMHq0EWCSsGAQQB2kcPAQEHQOcaXhhzcvb/kFG5fkbO\n" + - "zMdHOg/dd4JLFtXmeEHoN9QdiNUEGBYKAH0FAmMHq0ECngECmwIFFgIDAQAECwkI\n" + - "BwUVCgkIC18gBBkWCgAGBQJjB6tBAAoJENH9GnI3A/RMIVMA/1GU9E+vA8bs0vJV\n" + - "Djp1ri3J4S7u+abwmlivDw8g8XCWAPwKWWfHLgJCsAHkINuDgJdqbNPATFiXxH9F\n" + - "qYnOvWy6DAAKCRBoj2Vso6Fps884AP9D5ZOwuBEXyT/j0G8CWBZ0lT14kRGFucjQ\n" + - "i9kZStAuVgEA5cd3eUWofnekd/P6R3UgmvhVOqvxwUUgY3mEArH7+waYMwRjB6tB\n" + - "FgkrBgEEAdpHDwEBB0BCYWjTs0pfBnKYgO0O07djiMSBtUJVpUFo6zrVK92RgLQU\n" + - "QiA8YkBwZ3BhaW5sZXNzLm9yZz6IjwQTFgoAQQUCYwerQQkQCewToVNMc2UWIQTb\n" + - "8vkcFtrogUMe7xwJ7BOhU0xzZQKeAQKbAQUWAgMBAAQLCQgHBRUKCQgLApkBAABL\n" + - "swEAjvi5gsrtdHqgLBZiTsLqfB2C7e3jvwxoSlUdDP/2cQYBAK8kJiqo53pwAbp/\n" + - "AVogneH339jmjY1qI3XtLbM5cRkMuDgEYwerQRIKKwYBBAGXVQEFAQEHQFQSNdAJ\n" + - "PjHAmvKSxewwtoWjjekVPBZpPN26JaALLYoyAwEIB4h1BBgWCgAdBQJjB6tBAp4B\n" + - "ApsMBRYCAwEABAsJCAcFFQoJCAsACgkQCewToVNMc2X+ZQEAzoMzIaoN3xPB4OB2\n" + - "7ePgdHwA9JKehCYLCkVk+ym2dEkA/2lRhb+1UVaooqjk/2DECgQcB56vwhiasVMF\n" + - "JmrwTEQKuDMEYwerQRYJKwYBBAHaRw8BAQdAOZL926bJ5cbNi6wM3gceEYLV0DQY\n" + - "RkYc5KNbaOKvnm2I1QQYFgoAfQUCYwerQQKeAQKbAgUWAgMBAAQLCQgHBRUKCQgL\n" + - "XyAEGRYKAAYFAmMHq0EACgkQpYWdiAVpxGRW4AD+Lade9kJrvcBMSq8EERhYTH6D\n" + - "Fka4eMgFB76kH31WmpQA+gOU7kwqKmtyVsXVgCLGMcdTvbZr+73C5m8R7LsdY5kE\n" + - "AAoJEAnsE6FTTHNl7BAA/2v8Wzfmg1OO6IWCohmmNgF4rIDBW8Q9s3+1I/mWlMyj\n" + - "AP9YGR+fnN/YOQrlSG9UiXE5fGwUhaPB0LEGWp0wmmQYA5gzBGMHq0EWCSsGAQQB\n" + - "2kcPAQEHQI8C53+C8crLCQ48OKQa1dEKc8XWQSA6Ckg5j73tOJRLtBRDIDxjQHBn\n" + - "cGFpbmxlc3Mub3JnPoiPBBMWCgBBBQJjB6tBCRCiaZcdjHWX3hYhBCn4yvDzoRXW\n" + - "bJSBQqJplx2MdZfeAp4BApsBBRYCAwEABAsJCAcFFQoJCAsCmQEAABDnAQC9qGed\n" + - "G7Bj5jsQ9JAb6yqGx29JO6aV5g+0Q6cd8py2mQD+JpOng6hTD9G3JZQCTwws3jsI\n" + - "BbnDCEVRtV9k5pLzzAy4OARjB6tBEgorBgEEAZdVAQUBAQdA3P+zrxkZQF8OYzPn\n" + - "XRwWAfP6YB0Ladd4TiEcpX8chWwDAQgHiHUEGBYKAB0FAmMHq0ECngECmwwFFgID\n" + - "AQAECwkIBwUVCgkICwAKCRCiaZcdjHWX3qs2AP4y7Y0ZsuNjAjsZxdwPeVmSA6BK\n" + - "IRVNEAHhP8lUyxqA0wD+MYU855XgASu1Ww/RiV3w+g5BAu+PbTrEk3mDvxjsSg64\n" + - "MwRjB6tBFgkrBgEEAdpHDwEBB0BZ78lkoDr+IJ9wod0WO+KUYPu4VzgSQH+sm8L8\n" + - "fbeKJYjVBBgWCgB9BQJjB6tBAp4BApsCBRYCAwEABAsJCAcFFQoJCAtfIAQZFgoA\n" + - "BgUCYwerQQAKCRB8jJGVps/ENgz7AP9ZMENJH+rIKMjynb9WPBlvJ8yJ9dMhzCxc\n" + - "ssxgEVZYXAEA5ZsE5xJLQC/cVMGFvqaQ8iPo5jhDZpQJ8RCVlb8XzQwACgkQommX\n" + - "HYx1l96SkgD/f0FYkK4yB8FWuntJ3n0FUfE31wDwpxvvpvP+o3d2GB4BAP9LRKBX\n" + - "Mwj4jzJc4ViKmwiNJAPttDQCpYjzJT7LUKAA\n" + - "=WaRm\n" + - "-----END PGP PUBLIC KEY BLOCK-----"; - private static final OpenPgpFingerprint cert1fp = OpenPgpFingerprint.parse("DA8AF595FC41F71BC4F26608688F656CA3A169B3"); - private static final OpenPgpFingerprint cert2fp = OpenPgpFingerprint.parse("DBF2F91C16DAE881431EEF1C09EC13A1534C7365"); - private static final OpenPgpFingerprint cert3fp = OpenPgpFingerprint.parse("29F8CAF0F3A115D66C948142A269971D8C7597DE"); - - @Test - public void encryptWithCertFromCertificateStore() throws PGPException, IOException, BadDataException, InterruptedException, BadNameException { - // In-Memory certificate store - PGPainlessCertD certificateDirectory = PGPainlessCertD.inMemory(); - PGPCertificateStoreAdapter adapter = new PGPCertificateStoreAdapter(certificateDirectory); - - // Populate store - PGPPublicKeyRingCollection certificates = PGPainless.readKeyRing().publicKeyRingCollection(CERT_COLLECTION); - for (PGPPublicKeyRing cert : certificates) { - certificateDirectory.insert(new ByteArrayInputStream(cert.getEncoded()), MergeCallbacks.mergeWithExisting()); - } - - // Encrypt message - ByteArrayOutputStream ciphertextOut = new ByteArrayOutputStream(); - EncryptionStream encryptionStream = PGPainless.encryptAndOrSign() - .onOutputStream(ciphertextOut) - .withOptions(ProducerOptions.encrypt( - EncryptionOptions.encryptCommunications() - .addRecipient(adapter, cert2fp))); - ByteArrayInputStream plaintext = new ByteArrayInputStream("Hello, World! This message is encrypted using a cert from a store!".getBytes()); - Streams.pipeAll(plaintext, encryptionStream); - encryptionStream.close(); - - // Get cert from store - Certificate cert = adapter.getCertificate(cert2fp.toString()); - PGPPublicKeyRing publicKeys = PGPainless.readKeyRing().publicKeyRing(cert.getInputStream()); - - // check if message was encrypted for cert - assertTrue(encryptionStream.getResult().isEncryptedFor(publicKeys)); - } - - @Test - public void verifyWithCertFromCertificateStore() - throws PGPException, IOException, BadDataException, InterruptedException, BadNameException { - // In-Memory certificate store - PGPainlessCertD certificateDirectory = PGPainlessCertD.inMemory(); - PGPCertificateStoreAdapter adapter = new PGPCertificateStoreAdapter(certificateDirectory); - - // Populate store - PGPPublicKeyRingCollection certificates = PGPainless.readKeyRing().publicKeyRingCollection(CERT_COLLECTION); - for (PGPPublicKeyRing cert : certificates) { - certificateDirectory.insert(new ByteArrayInputStream(cert.getEncoded()), MergeCallbacks.mergeWithExisting()); - } - - // Prepare keys - OpenPgpFingerprint cryptFp = cert3fp; - OpenPgpFingerprint signFp = cert1fp; - PGPSecretKeyRingCollection secretKeys = PGPainless.readKeyRing().secretKeyRingCollection(KEY_COLLECTION); - PGPSecretKeyRing signingKey = secretKeys.getSecretKeyRing(signFp.getKeyId()); - PGPSecretKeyRing decryptionKey = secretKeys.getSecretKeyRing(cryptFp.getKeyId()); - SecretKeyRingProtector protector = SecretKeyRingProtector.unprotectedKeys(); - - // Encrypt and sign message - ByteArrayInputStream plaintextIn = new ByteArrayInputStream( - "This message was encrypted with a cert from a store and gets verified with a cert from a store as well".getBytes()); - ByteArrayOutputStream ciphertext = new ByteArrayOutputStream(); - EncryptionStream encryptionStream = PGPainless.encryptAndOrSign() - .onOutputStream(ciphertext) - .withOptions( - ProducerOptions.signAndEncrypt( - EncryptionOptions.encryptCommunications() - .addRecipient(adapter, cryptFp), - SigningOptions.get() - .addSignature(protector, signingKey) - )); - Streams.pipeAll(plaintextIn, encryptionStream); - encryptionStream.close(); - - // Prepare ciphertext for decryption - ByteArrayInputStream ciphertextIn = new ByteArrayInputStream(ciphertext.toByteArray()); - ByteArrayOutputStream plaintextOut = new ByteArrayOutputStream(); - // Decrypt and verify - DecryptionStream decryptionStream = PGPainless.decryptAndOrVerify() - .onInputStream(ciphertextIn) - .withOptions( - new ConsumerOptions() - .addDecryptionKey(decryptionKey, protector) - .addVerificationCerts(adapter)); - Streams.pipeAll(decryptionStream, plaintextOut); - decryptionStream.close(); - - // Check that message can be decrypted and is verified - OpenPgpMetadata result = decryptionStream.getResult(); - assertTrue(result.isEncrypted()); - assertTrue(result.isVerified()); - assertTrue(result.containsVerifiedSignatureFrom(signFp)); - } -} diff --git a/version.gradle b/version.gradle index c6f7104e..53705861 100644 --- a/version.gradle +++ b/version.gradle @@ -17,8 +17,6 @@ allprojects { junitVersion = '5.8.2' logbackVersion = '1.2.11' mockitoVersion = '4.5.1' - pgpainlessCertDVersion = '0.2.0' - pgpCertDJavaVersion = '0.2.1' slf4jVersion = '1.7.36' sopJavaVersion = '4.0.7' } From 4bf2e07ddbab6f00f2f6b079948fabd61439bc3b Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Fri, 13 Jan 2023 19:30:35 +0100 Subject: [PATCH 216/763] Bump sop-java to 4.1.0 --- version.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.gradle b/version.gradle index 53705861..fd3129e3 100644 --- a/version.gradle +++ b/version.gradle @@ -18,6 +18,6 @@ allprojects { logbackVersion = '1.2.11' mockitoVersion = '4.5.1' slf4jVersion = '1.7.36' - sopJavaVersion = '4.0.7' + sopJavaVersion = '4.1.0' } } From 75feb167f0a12756f5b99c0f281fd0c2206fe5a5 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Fri, 13 Jan 2023 19:30:41 +0100 Subject: [PATCH 217/763] Update CHANGELOG --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d5be2adc..25272b18 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,7 +8,8 @@ SPDX-License-Identifier: CC0-1.0 ## 1.4.2-SNAPSHOT - Properly decrypt messages without MDC packets when `ConsumerOptions.setIgnoreMDCErrors(true)` is set - Fix crash in `sop generate-key --with-key-password` when more than one user-id is given - +- Revert integration with `pgp-certificate-store` +- Bump `sop-java` to `4.1.0` ## 1.4.1 - Add `UserId.parse()` method to parse user-ids into their components From 2d33a7e5d3cefa110608b6c4a70ae04c58c85fe2 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Fri, 13 Jan 2023 19:33:26 +0100 Subject: [PATCH 218/763] PGPainless 1.4.2 --- CHANGELOG.md | 2 +- README.md | 2 +- pgpainless-sop/README.md | 4 ++-- version.gradle | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 25272b18..370dace7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ SPDX-License-Identifier: CC0-1.0 # PGPainless Changelog -## 1.4.2-SNAPSHOT +## 1.4.2 - Properly decrypt messages without MDC packets when `ConsumerOptions.setIgnoreMDCErrors(true)` is set - Fix crash in `sop generate-key --with-key-password` when more than one user-id is given - Revert integration with `pgp-certificate-store` diff --git a/README.md b/README.md index 49320f94..3b6dc7cb 100644 --- a/README.md +++ b/README.md @@ -191,7 +191,7 @@ repositories { } dependencies { - implementation 'org.pgpainless:pgpainless-core:1.4.1' + implementation 'org.pgpainless:pgpainless-core:1.4.2' } ``` diff --git a/pgpainless-sop/README.md b/pgpainless-sop/README.md index c1b2a988..97b7765a 100644 --- a/pgpainless-sop/README.md +++ b/pgpainless-sop/README.md @@ -23,7 +23,7 @@ To start using pgpainless-sop in your code, include the following lines in your ... dependencies { ... - implementation "org.pgpainless:pgpainless-sop:1.4.1" + implementation "org.pgpainless:pgpainless-sop:1.4.2" ... } @@ -34,7 +34,7 @@ dependencies { org.pgpainless pgpainless-sop - 1.4.1 + 1.4.2 ... diff --git a/version.gradle b/version.gradle index fd3129e3..83ea0d7d 100644 --- a/version.gradle +++ b/version.gradle @@ -5,7 +5,7 @@ allprojects { ext { shortVersion = '1.4.2' - isSnapshot = true + isSnapshot = false pgpainlessMinAndroidSdk = 10 javaSourceCompatibility = 1.8 bouncyCastleVersion = '1.72' From 6c0bb6c6272166e0cd53f766b9126afc9c7f0f74 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Fri, 13 Jan 2023 19:38:37 +0100 Subject: [PATCH 219/763] PGPainless 1.4.3-SNAPSHOT --- version.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/version.gradle b/version.gradle index 83ea0d7d..b49f7930 100644 --- a/version.gradle +++ b/version.gradle @@ -4,8 +4,8 @@ allprojects { ext { - shortVersion = '1.4.2' - isSnapshot = false + shortVersion = '1.4.3' + isSnapshot = true pgpainlessMinAndroidSdk = 10 javaSourceCompatibility = 1.8 bouncyCastleVersion = '1.72' From b58861635d4206909f2c0b65b47af0f04d08a7fe Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 16 Jan 2023 19:38:52 +0100 Subject: [PATCH 220/763] Add some missing javadoc --- build.gradle | 12 ++++++++++++ .../java/org/pgpainless/cli/PGPainlessCLI.java | 10 ++++++++++ .../main/java/org/pgpainless/sop/ArmorImpl.java | 3 +++ .../java/org/pgpainless/sop/DearmorImpl.java | 3 +++ .../java/org/pgpainless/sop/DecryptImpl.java | 3 +++ .../org/pgpainless/sop/DetachedSignImpl.java | 3 +++ .../org/pgpainless/sop/DetachedVerifyImpl.java | 3 +++ .../java/org/pgpainless/sop/EncryptImpl.java | 3 +++ .../org/pgpainless/sop/ExtractCertImpl.java | 3 +++ .../org/pgpainless/sop/GenerateKeyImpl.java | 3 +++ .../org/pgpainless/sop/InlineDetachImpl.java | 3 +++ .../java/org/pgpainless/sop/InlineSignImpl.java | 3 +++ .../org/pgpainless/sop/InlineVerifyImpl.java | 3 +++ .../main/java/org/pgpainless/sop/KeyReader.java | 3 +++ .../sop/MatchMakingSecretKeyRingProtector.java | 17 +++++++++++++++++ .../main/java/org/pgpainless/sop/SOPImpl.java | 6 ++++++ .../java/org/pgpainless/sop/VersionImpl.java | 3 +++ 17 files changed, 84 insertions(+) diff --git a/build.gradle b/build.gradle index 6e086754..eb4ff723 100644 --- a/build.gradle +++ b/build.gradle @@ -258,6 +258,18 @@ task javadocAll(type: Javadoc) { ] as String[] } +if (JavaVersion.current().isJava8Compatible()) { + tasks.withType(Javadoc) { + // The '-quiet' as second argument is actually a hack, + // since the one paramater addStringOption doesn't seem to + // work, we extra add '-quiet', which is added anyway by + // gradle. See https://github.com/gradle/gradle/issues/2354 + // See JDK-8200363 (https://bugs.openjdk.java.net/browse/JDK-8200363) + // for information about the -Xwerror option. + options.addStringOption('Xwerror', '-quiet') + } +} + /** * Fetch sha256 checksums of artifacts published to maven central. * diff --git a/pgpainless-cli/src/main/java/org/pgpainless/cli/PGPainlessCLI.java b/pgpainless-cli/src/main/java/org/pgpainless/cli/PGPainlessCLI.java index 35791a3e..938bf1aa 100644 --- a/pgpainless-cli/src/main/java/org/pgpainless/cli/PGPainlessCLI.java +++ b/pgpainless-cli/src/main/java/org/pgpainless/cli/PGPainlessCLI.java @@ -18,6 +18,10 @@ public class PGPainlessCLI { SopCLI.setSopInstance(new SOPImpl()); } + /** + * Main method of the CLI application. + * @param args arguments + */ public static void main(String[] args) { int result = execute(args); if (result != 0) { @@ -25,6 +29,12 @@ public class PGPainlessCLI { } } + /** + * Execute the given command and return the exit code of the program. + * + * @param args command string array (e.g. ["pgpainless-cli", "generate-key", "Alice"]) + * @return exit code + */ public static int execute(String... args) { return SopCLI.execute(args); } diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/ArmorImpl.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/ArmorImpl.java index d65c2925..daee3a9b 100644 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/ArmorImpl.java +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/ArmorImpl.java @@ -18,6 +18,9 @@ import sop.enums.ArmorLabel; import sop.exception.SOPGPException; import sop.operation.Armor; +/** + * Implementation of the
      armor
      operation using PGPainless. + */ public class ArmorImpl implements Armor { @Override diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/DearmorImpl.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/DearmorImpl.java index f0b21a6d..29483437 100644 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/DearmorImpl.java +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/DearmorImpl.java @@ -15,6 +15,9 @@ import sop.Ready; import sop.exception.SOPGPException; import sop.operation.Dearmor; +/** + * Implementation of the
      dearmor
      operation using PGPainless. + */ public class DearmorImpl implements Dearmor { @Override diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/DecryptImpl.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/DecryptImpl.java index 867024ef..f7876799 100644 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/DecryptImpl.java +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/DecryptImpl.java @@ -34,6 +34,9 @@ import sop.Verification; import sop.exception.SOPGPException; import sop.operation.Decrypt; +/** + * Implementation of the
      decrypt
      operation using PGPainless. + */ public class DecryptImpl implements Decrypt { private final ConsumerOptions consumerOptions = ConsumerOptions.get(); diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/DetachedSignImpl.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/DetachedSignImpl.java index f6ab8172..c32cb219 100644 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/DetachedSignImpl.java +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/DetachedSignImpl.java @@ -36,6 +36,9 @@ import sop.enums.SignAs; import sop.exception.SOPGPException; import sop.operation.DetachedSign; +/** + * Implementation of the
      sign
      operation using PGPainless. + */ public class DetachedSignImpl implements DetachedSign { private boolean armor = true; diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/DetachedVerifyImpl.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/DetachedVerifyImpl.java index 1ed43941..93ad398c 100644 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/DetachedVerifyImpl.java +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/DetachedVerifyImpl.java @@ -23,6 +23,9 @@ import sop.Verification; import sop.exception.SOPGPException; import sop.operation.DetachedVerify; +/** + * Implementation of the
      verify
      operation using PGPainless. + */ public class DetachedVerifyImpl implements DetachedVerify { private final ConsumerOptions options = ConsumerOptions.get(); diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/EncryptImpl.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/EncryptImpl.java index 61a46731..1874d9e1 100644 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/EncryptImpl.java +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/EncryptImpl.java @@ -34,6 +34,9 @@ import sop.exception.SOPGPException; import sop.operation.Encrypt; import sop.util.ProxyOutputStream; +/** + * Implementation of the
      encrypt
      operation using PGPainless. + */ public class EncryptImpl implements Encrypt { EncryptionOptions encryptionOptions = EncryptionOptions.get(); diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/ExtractCertImpl.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/ExtractCertImpl.java index 5be3ad31..be7fc9c3 100644 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/ExtractCertImpl.java +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/ExtractCertImpl.java @@ -19,6 +19,9 @@ import sop.Ready; import sop.exception.SOPGPException; import sop.operation.ExtractCert; +/** + * Implementation of the
      extract-cert
      operation using PGPainless. + */ public class ExtractCertImpl implements ExtractCert { private boolean armor = true; diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/GenerateKeyImpl.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/GenerateKeyImpl.java index 70561693..da99c854 100644 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/GenerateKeyImpl.java +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/GenerateKeyImpl.java @@ -24,6 +24,9 @@ import sop.Ready; import sop.exception.SOPGPException; import sop.operation.GenerateKey; +/** + * Implementation of the
      generate-key
      operation using PGPainless. + */ public class GenerateKeyImpl implements GenerateKey { private boolean armor = true; diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/InlineDetachImpl.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/InlineDetachImpl.java index 178018aa..bafc2794 100644 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/InlineDetachImpl.java +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/InlineDetachImpl.java @@ -30,6 +30,9 @@ import sop.Signatures; import sop.exception.SOPGPException; import sop.operation.InlineDetach; +/** + * Implementation of the
      inline-detach
      operation using PGPainless. + */ public class InlineDetachImpl implements InlineDetach { private boolean armor = true; diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/InlineSignImpl.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/InlineSignImpl.java index 82c3603b..dd4ab0cf 100644 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/InlineSignImpl.java +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/InlineSignImpl.java @@ -29,6 +29,9 @@ import sop.enums.InlineSignAs; import sop.exception.SOPGPException; import sop.operation.InlineSign; +/** + * Implementation of the
      inline-sign
      operation using PGPainless. + */ public class InlineSignImpl implements InlineSign { private boolean armor = true; diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/InlineVerifyImpl.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/InlineVerifyImpl.java index f33f718b..7665a7bb 100644 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/InlineVerifyImpl.java +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/InlineVerifyImpl.java @@ -26,6 +26,9 @@ import sop.Verification; import sop.exception.SOPGPException; import sop.operation.InlineVerify; +/** + * Implementation of the
      inline-verify
      operation using PGPainless. + */ public class InlineVerifyImpl implements InlineVerify { private final ConsumerOptions options = ConsumerOptions.get(); diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/KeyReader.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/KeyReader.java index 5e6f3a7d..036ec126 100644 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/KeyReader.java +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/KeyReader.java @@ -13,6 +13,9 @@ import sop.exception.SOPGPException; import java.io.IOException; import java.io.InputStream; +/** + * Reader for OpenPGP keys and certificates with error matching according to the SOP spec. + */ class KeyReader { static PGPSecretKeyRingCollection readSecretKeys(InputStream keyInputStream, boolean requireContent) diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/MatchMakingSecretKeyRingProtector.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/MatchMakingSecretKeyRingProtector.java index df54583e..0b88cb5d 100644 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/MatchMakingSecretKeyRingProtector.java +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/MatchMakingSecretKeyRingProtector.java @@ -20,12 +20,21 @@ import org.pgpainless.key.protection.SecretKeyRingProtector; import org.pgpainless.key.protection.UnlockSecretKey; import org.pgpainless.util.Passphrase; +/** + * Implementation of the {@link SecretKeyRingProtector} which can be handed passphrases and keys separately, + * and which then matches up passphrases and keys when needed. + */ public class MatchMakingSecretKeyRingProtector implements SecretKeyRingProtector { private final Set passphrases = new HashSet<>(); private final Set keys = new HashSet<>(); private final CachingSecretKeyRingProtector protector = new CachingSecretKeyRingProtector(); + /** + * Add a single passphrase to the protector. + * + * @param passphrase passphrase + */ public void addPassphrase(Passphrase passphrase) { if (passphrase.isEmpty()) { return; @@ -46,6 +55,11 @@ public class MatchMakingSecretKeyRingProtector implements SecretKeyRingProtector } } + /** + * Add a single {@link PGPSecretKeyRing} to the protector. + * + * @param key secret keys + */ public void addSecretKey(PGPSecretKeyRing key) { if (!keys.add(key)) { return; @@ -89,6 +103,9 @@ public class MatchMakingSecretKeyRingProtector implements SecretKeyRingProtector return protector.getEncryptor(keyId); } + /** + * Clear all known passphrases from the protector. + */ public void clear() { for (Passphrase passphrase : passphrases) { passphrase.clear(); diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/SOPImpl.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/SOPImpl.java index 28772f10..a49f7e34 100644 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/SOPImpl.java +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/SOPImpl.java @@ -19,6 +19,12 @@ import sop.operation.InlineSign; import sop.operation.InlineVerify; import sop.operation.Version; +/** + * Implementation of the
      sop
      API using PGPainless. + *
       {@code
      + * SOP sop = new SOPImpl();
      + * }
      + */ public class SOPImpl implements SOP { static { diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/VersionImpl.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/VersionImpl.java index dbd1cf35..4449af10 100644 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/VersionImpl.java +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/VersionImpl.java @@ -12,6 +12,9 @@ import java.util.Properties; import org.bouncycastle.jce.provider.BouncyCastleProvider; import sop.operation.Version; +/** + * Implementation of the
      version
      operation using PGPainless. + */ public class VersionImpl implements Version { // draft version From a50c2d97142c30b492d7a57a6a352ef957cfa3d7 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 16 Jan 2023 20:15:57 +0100 Subject: [PATCH 221/763] More missing javadoc --- .../pgpainless/algorithm/AEADAlgorithm.java | 5 +++++ .../protection/BaseSecretKeyRingProtector.java | 16 ++++++++++++++++ .../key/protection/SecretKeyRingProtector.java | 6 ++++++ .../java/org/pgpainless/util/ArmorUtils.java | 18 ++++++++++++++++++ .../util/ArmoredInputStreamFactory.java | 11 +++++++++++ .../util/ArmoredOutputStreamFactory.java | 10 ++++++++++ 6 files changed, 66 insertions(+) diff --git a/pgpainless-core/src/main/java/org/pgpainless/algorithm/AEADAlgorithm.java b/pgpainless-core/src/main/java/org/pgpainless/algorithm/AEADAlgorithm.java index 106d6bff..a5885005 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/algorithm/AEADAlgorithm.java +++ b/pgpainless-core/src/main/java/org/pgpainless/algorithm/AEADAlgorithm.java @@ -41,6 +41,11 @@ public enum AEADAlgorithm { this.tagLength = tagLength; } + /** + * Return the ID of the AEAD algorithm. + * + * @return algorithm ID + */ public int getAlgorithmId() { return algorithmId; } diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/protection/BaseSecretKeyRingProtector.java b/pgpainless-core/src/main/java/org/pgpainless/key/protection/BaseSecretKeyRingProtector.java index 5b545c12..1a31d0e8 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/key/protection/BaseSecretKeyRingProtector.java +++ b/pgpainless-core/src/main/java/org/pgpainless/key/protection/BaseSecretKeyRingProtector.java @@ -13,15 +13,31 @@ import org.pgpainless.util.Passphrase; import javax.annotation.Nullable; +/** + * Basic {@link SecretKeyRingProtector} implementation that respects the users {@link KeyRingProtectionSettings} when + * encrypting keys. + */ public class BaseSecretKeyRingProtector implements SecretKeyRingProtector { private final SecretKeyPassphraseProvider passphraseProvider; private final KeyRingProtectionSettings protectionSettings; + /** + * Constructor that uses the given {@link SecretKeyPassphraseProvider} to retrieve passphrases and PGPainless' + * default {@link KeyRingProtectionSettings}. + * + * @param passphraseProvider provider for passphrases + */ public BaseSecretKeyRingProtector(SecretKeyPassphraseProvider passphraseProvider) { this(passphraseProvider, KeyRingProtectionSettings.secureDefaultSettings()); } + /** + * Constructor that uses the given {@link SecretKeyPassphraseProvider} and {@link KeyRingProtectionSettings}. + * + * @param passphraseProvider provider for passphrases + * @param protectionSettings protection settings + */ public BaseSecretKeyRingProtector(SecretKeyPassphraseProvider passphraseProvider, KeyRingProtectionSettings protectionSettings) { this.passphraseProvider = passphraseProvider; this.protectionSettings = protectionSettings; 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 ee461a4e..d7ed5c85 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 @@ -29,6 +29,12 @@ import org.pgpainless.util.Passphrase; */ public interface SecretKeyRingProtector { + /** + * Returns true, if the protector has a passphrase for the key with the given key-id. + * + * @param keyId key id + * @return true if it has a passphrase, false otherwise + */ boolean hasPassphraseFor(Long keyId); /** diff --git a/pgpainless-core/src/main/java/org/pgpainless/util/ArmorUtils.java b/pgpainless-core/src/main/java/org/pgpainless/util/ArmorUtils.java index b3e60023..87170638 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/util/ArmorUtils.java +++ b/pgpainless-core/src/main/java/org/pgpainless/util/ArmorUtils.java @@ -33,15 +33,33 @@ import org.pgpainless.algorithm.HashAlgorithm; import org.pgpainless.decryption_verification.OpenPgpInputStream; import org.pgpainless.key.OpenPgpFingerprint; +/** + * Utility class for dealing with ASCII armored OpenPGP data. + */ public final class ArmorUtils { // MessageIDs are 32 printable characters private static final Pattern PATTERN_MESSAGE_ID = Pattern.compile("^\\S{32}$"); + /** + * Constant armor key for comments. + */ public static final String HEADER_COMMENT = "Comment"; + /** + * Constant armor key for program versions. + */ public static final String HEADER_VERSION = "Version"; + /** + * Constant armor key for message IDs. Useful for split messages. + */ public static final String HEADER_MESSAGEID = "MessageID"; + /** + * Constant armor key for used hash algorithms in clearsigned messages. + */ public static final String HEADER_HASH = "Hash"; + /** + * Constant armor key for message character sets. + */ public static final String HEADER_CHARSET = "Charset"; private ArmorUtils() { diff --git a/pgpainless-core/src/main/java/org/pgpainless/util/ArmoredInputStreamFactory.java b/pgpainless-core/src/main/java/org/pgpainless/util/ArmoredInputStreamFactory.java index 0c5ceeaf..e48fb4c0 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/util/ArmoredInputStreamFactory.java +++ b/pgpainless-core/src/main/java/org/pgpainless/util/ArmoredInputStreamFactory.java @@ -9,12 +9,23 @@ import java.io.InputStream; import org.bouncycastle.bcpg.ArmoredInputStream; +/** + * Factory class for instantiating preconfigured {@link ArmoredInputStream ArmoredInputStreams}. + * {@link #get(InputStream)} will return an {@link ArmoredInputStream} that is set up to properly detect CRC errors. + */ public final class ArmoredInputStreamFactory { private ArmoredInputStreamFactory() { } + /** + * Return an instance of {@link ArmoredInputStream} which will detect CRC errors. + * + * @param inputStream input stream + * @return armored input stream + * @throws IOException in case of an IO error + */ public static ArmoredInputStream get(InputStream inputStream) throws IOException { if (inputStream instanceof CRCingArmoredInputStreamWrapper) { return (ArmoredInputStream) inputStream; diff --git a/pgpainless-core/src/main/java/org/pgpainless/util/ArmoredOutputStreamFactory.java b/pgpainless-core/src/main/java/org/pgpainless/util/ArmoredOutputStreamFactory.java index fb2cd4f5..f61bacb1 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/util/ArmoredOutputStreamFactory.java +++ b/pgpainless-core/src/main/java/org/pgpainless/util/ArmoredOutputStreamFactory.java @@ -15,6 +15,9 @@ import org.pgpainless.encryption_signing.ProducerOptions; */ public final class ArmoredOutputStreamFactory { + /** + * Name of the program. + */ public static final String PGPAINLESS = "PGPainless"; private static String version = PGPAINLESS; private static String[] comment = new String[0]; @@ -42,6 +45,13 @@ public final class ArmoredOutputStreamFactory { return armoredOutputStream; } + /** + * Return an instance of the {@link ArmoredOutputStream} which might have pre-populated armor headers. + * + * @param outputStream output stream + * @param options options + * @return armored output stream + */ public static ArmoredOutputStream get(OutputStream outputStream, ProducerOptions options) { if (options.isHideArmorHeaders()) { ArmoredOutputStream armorOut = new ArmoredOutputStream(outputStream); From 4cf5a32cc02bc9d3c1f83425e68b86564e9e7ec6 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Sat, 21 Jan 2023 19:17:49 +0100 Subject: [PATCH 222/763] Update SECURITY.md --- SECURITY.md | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/SECURITY.md b/SECURITY.md index fad439b5..0549d1a0 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -12,12 +12,11 @@ SPDX-License-Identifier: Apache-2.0 Use this section to tell people about which versions of your project are currently being supported with security updates. -| Version | Supported | -|----------|--------------------| -| 1.4.X-rc | :white_check_mark: | -| 1.3.X | :white_check_mark: | -| 1.2.X | :white_check_mark: | -| < 1.2.0 | :x: | +| Version | Supported | +|---------|--------------------| +| 1.4.X | :white_check_mark: | +| 1.3.X | :white_check_mark: | +| < 1.3.X | :x: | ## Reporting a Vulnerability From 67cc59efa26b33696381eeab1cd318c505fe7bd5 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Sat, 21 Jan 2023 19:16:39 +0100 Subject: [PATCH 223/763] Update changelog --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 370dace7..62740014 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -62,6 +62,10 @@ SPDX-License-Identifier: CC0-1.0 - Add `KeyRingUtils.publicKeys(PGPKeyRing keys)` - Remove `BCUtil` class +## 1.3.16 +- Bump `sop-java` to `4.1.0` +- Bump `gradlew` to `7.5` + ## 1.3.15 - Fix crash in `sop generate-key --with-key-password` when more than one user-id is given - `sop generate-key`: Allow key generation without user-ids From 9f98e4ce371670d8cc05c5e9cdbd095d1b899271 Mon Sep 17 00:00:00 2001 From: DenBond7 Date: Mon, 23 Jan 2023 10:02:02 +0200 Subject: [PATCH 224/763] Fixed redundant dot an exception message. --- .../decryption_verification/OpenPgpMessageInputStream.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java index bb5d67b3..7fe11bbf 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java @@ -970,7 +970,7 @@ public class OpenPgpMessageInputStream extends DecryptionStream { LOGGER.debug("No suitable certificate for verification of signature by key " + KeyIdUtil.formatKeyId(keyId) + " found."); inbandSignaturesWithMissingCert.add(new SignatureVerification.Failure( new SignatureVerification(signature, null), - new SignatureValidationException("Missing verification key."))); + new SignatureValidationException("Missing verification key"))); } } From 695e03f8b6cc01e0fc98b6ce2d9894f9f5e605d2 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 31 Jan 2023 18:19:08 +0100 Subject: [PATCH 225/763] Add EncryptionOptions.hasEncryptionMethod() --- .../encryption_signing/EncryptionOptions.java | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/EncryptionOptions.java b/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/EncryptionOptions.java index 612dcd50..b5baee83 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/EncryptionOptions.java +++ b/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/EncryptionOptions.java @@ -311,6 +311,16 @@ public class EncryptionOptions { return this; } + /** + * Return
      true
      iff the user specified at least one encryption method, + *
      false
      otherwise. + * + * @return encryption methods is not empty + */ + public boolean hasEncryptionMethod() { + return !encryptionMethods.isEmpty(); + } + public interface EncryptionKeySelector { List selectEncryptionSubkeys(List encryptionCapableKeys); } From 83ef9cfe80cfbf12d59f7ad7beac3f7eda6c2631 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 31 Jan 2023 18:20:03 +0100 Subject: [PATCH 226/763] SOP encrypt: Throw MissingArg if no encryption method was provided. --- .../src/main/java/org/pgpainless/sop/EncryptImpl.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/EncryptImpl.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/EncryptImpl.java index 1874d9e1..62d20bf0 100644 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/EncryptImpl.java +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/EncryptImpl.java @@ -113,6 +113,9 @@ public class EncryptImpl implements Encrypt { @Override public Ready plaintext(InputStream plaintext) throws IOException { + if (!encryptionOptions.hasEncryptionMethod()) { + throw new SOPGPException.MissingArg("Missing encryption method."); + } ProducerOptions producerOptions = signingOptions != null ? ProducerOptions.signAndEncrypt(encryptionOptions, signingOptions) : ProducerOptions.encrypt(encryptionOptions); From f4bd17ade8f253575c47dff03492b306c4364944 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 31 Jan 2023 19:02:49 +0100 Subject: [PATCH 227/763] Bump sop-java to 4.1.1 --- version.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.gradle b/version.gradle index b49f7930..7154bca5 100644 --- a/version.gradle +++ b/version.gradle @@ -18,6 +18,6 @@ allprojects { logbackVersion = '1.2.11' mockitoVersion = '4.5.1' slf4jVersion = '1.7.36' - sopJavaVersion = '4.1.0' + sopJavaVersion = '4.1.1' } } From d53cd6d0bdf7e8740de7359f14d1a9f2fc591222 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 31 Jan 2023 19:03:11 +0100 Subject: [PATCH 228/763] pgpainless-sop: reuse shared sop-java test suite --- pgpainless-sop/build.gradle | 4 +++ .../PGPainlessSopInstanceFactory.java | 20 ++++++++++++ .../operation/PGPainlessArmorDearmorTest.java | 11 +++++++ .../PGPainlessDecryptWIthSessionKeyTest.java | 11 +++++++ ...ainlessDetachedSignDetachedVerifyTest.java | 18 +++++++++++ .../PGPainlessEncryptDecryptTest.java | 11 +++++++ .../operation/PGPainlessExtractCertTest.java | 32 +++++++++++++++++++ .../operation/PGPainlessGenerateKeyTest.java | 11 +++++++ ...ineSignInlineDetachDetachedVerifyTest.java | 12 +++++++ .../PGPainlessInlineSignInlineVerifyTest.java | 11 +++++++ .../operation/PGPainlessVersionTest.java | 11 +++++++ 11 files changed, 152 insertions(+) create mode 100644 pgpainless-sop/src/test/java/sop/testsuite/pgpainless/PGPainlessSopInstanceFactory.java create mode 100644 pgpainless-sop/src/test/java/sop/testsuite/pgpainless/operation/PGPainlessArmorDearmorTest.java create mode 100644 pgpainless-sop/src/test/java/sop/testsuite/pgpainless/operation/PGPainlessDecryptWIthSessionKeyTest.java create mode 100644 pgpainless-sop/src/test/java/sop/testsuite/pgpainless/operation/PGPainlessDetachedSignDetachedVerifyTest.java create mode 100644 pgpainless-sop/src/test/java/sop/testsuite/pgpainless/operation/PGPainlessEncryptDecryptTest.java create mode 100644 pgpainless-sop/src/test/java/sop/testsuite/pgpainless/operation/PGPainlessExtractCertTest.java create mode 100644 pgpainless-sop/src/test/java/sop/testsuite/pgpainless/operation/PGPainlessGenerateKeyTest.java create mode 100644 pgpainless-sop/src/test/java/sop/testsuite/pgpainless/operation/PGPainlessInlineSignInlineDetachDetachedVerifyTest.java create mode 100644 pgpainless-sop/src/test/java/sop/testsuite/pgpainless/operation/PGPainlessInlineSignInlineVerifyTest.java create mode 100644 pgpainless-sop/src/test/java/sop/testsuite/pgpainless/operation/PGPainlessVersionTest.java diff --git a/pgpainless-sop/build.gradle b/pgpainless-sop/build.gradle index 4eca8a7f..1a40bb27 100644 --- a/pgpainless-sop/build.gradle +++ b/pgpainless-sop/build.gradle @@ -21,10 +21,14 @@ dependencies { // Logging testImplementation "ch.qos.logback:logback-classic:$logbackVersion" + // Depend on "shared" sop-java test suite (fixtures are turned into tests by inheritance inside test sources) + testImplementation(testFixtures("org.pgpainless:sop-java:$sopJavaVersion")) + implementation(project(":pgpainless-core")) api "org.pgpainless:sop-java:$sopJavaVersion" } test { useJUnitPlatform() + environment("test.implementation", "sop.testsuite.pgpainless.PGPainlessSopInstanceFactory") } diff --git a/pgpainless-sop/src/test/java/sop/testsuite/pgpainless/PGPainlessSopInstanceFactory.java b/pgpainless-sop/src/test/java/sop/testsuite/pgpainless/PGPainlessSopInstanceFactory.java new file mode 100644 index 00000000..a9aac0e9 --- /dev/null +++ b/pgpainless-sop/src/test/java/sop/testsuite/pgpainless/PGPainlessSopInstanceFactory.java @@ -0,0 +1,20 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.testsuite.pgpainless; + +import java.util.Collections; +import java.util.Map; + +import org.pgpainless.sop.SOPImpl; +import sop.SOP; +import sop.testsuite.SOPInstanceFactory; + +public class PGPainlessSopInstanceFactory extends SOPInstanceFactory { + + @Override + public Map provideSOPInstances() { + return Collections.singletonMap("PGPainless-SOP", new SOPImpl()); + } +} diff --git a/pgpainless-sop/src/test/java/sop/testsuite/pgpainless/operation/PGPainlessArmorDearmorTest.java b/pgpainless-sop/src/test/java/sop/testsuite/pgpainless/operation/PGPainlessArmorDearmorTest.java new file mode 100644 index 00000000..9161706d --- /dev/null +++ b/pgpainless-sop/src/test/java/sop/testsuite/pgpainless/operation/PGPainlessArmorDearmorTest.java @@ -0,0 +1,11 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.testsuite.pgpainless.operation; + +import sop.testsuite.operation.ArmorDearmorTest; + +public class PGPainlessArmorDearmorTest extends ArmorDearmorTest { + +} diff --git a/pgpainless-sop/src/test/java/sop/testsuite/pgpainless/operation/PGPainlessDecryptWIthSessionKeyTest.java b/pgpainless-sop/src/test/java/sop/testsuite/pgpainless/operation/PGPainlessDecryptWIthSessionKeyTest.java new file mode 100644 index 00000000..2825db5f --- /dev/null +++ b/pgpainless-sop/src/test/java/sop/testsuite/pgpainless/operation/PGPainlessDecryptWIthSessionKeyTest.java @@ -0,0 +1,11 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.testsuite.pgpainless.operation; + +import sop.testsuite.operation.DecryptWithSessionKeyTest; + +public class PGPainlessDecryptWIthSessionKeyTest extends DecryptWithSessionKeyTest { + +} diff --git a/pgpainless-sop/src/test/java/sop/testsuite/pgpainless/operation/PGPainlessDetachedSignDetachedVerifyTest.java b/pgpainless-sop/src/test/java/sop/testsuite/pgpainless/operation/PGPainlessDetachedSignDetachedVerifyTest.java new file mode 100644 index 00000000..dff9e86f --- /dev/null +++ b/pgpainless-sop/src/test/java/sop/testsuite/pgpainless/operation/PGPainlessDetachedSignDetachedVerifyTest.java @@ -0,0 +1,18 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.testsuite.pgpainless.operation; + +import org.junit.jupiter.api.Disabled; +import sop.SOP; +import sop.testsuite.operation.DetachedSignDetachedVerifyTest; + +public class PGPainlessDetachedSignDetachedVerifyTest extends DetachedSignDetachedVerifyTest { + + @Override + @Disabled("Since we allow for dynamic cert loading, we can ignore this test") + public void verifyMissingCertCausesMissingArg(SOP sop) { + super.verifyMissingCertCausesMissingArg(sop); + } +} diff --git a/pgpainless-sop/src/test/java/sop/testsuite/pgpainless/operation/PGPainlessEncryptDecryptTest.java b/pgpainless-sop/src/test/java/sop/testsuite/pgpainless/operation/PGPainlessEncryptDecryptTest.java new file mode 100644 index 00000000..b6264d3a --- /dev/null +++ b/pgpainless-sop/src/test/java/sop/testsuite/pgpainless/operation/PGPainlessEncryptDecryptTest.java @@ -0,0 +1,11 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.testsuite.pgpainless.operation; + +import sop.testsuite.operation.EncryptDecryptTest; + +public class PGPainlessEncryptDecryptTest extends EncryptDecryptTest { + +} diff --git a/pgpainless-sop/src/test/java/sop/testsuite/pgpainless/operation/PGPainlessExtractCertTest.java b/pgpainless-sop/src/test/java/sop/testsuite/pgpainless/operation/PGPainlessExtractCertTest.java new file mode 100644 index 00000000..ee4e9684 --- /dev/null +++ b/pgpainless-sop/src/test/java/sop/testsuite/pgpainless/operation/PGPainlessExtractCertTest.java @@ -0,0 +1,32 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.testsuite.pgpainless.operation; + +import java.io.IOException; + +import org.junit.jupiter.api.Disabled; +import sop.SOP; +import sop.testsuite.operation.ExtractCertTest; + +public class PGPainlessExtractCertTest extends ExtractCertTest { + + @Disabled("BC uses old CTBs causing mismatching byte arrays :/") + @Override + public void extractAliceCertFromAliceKeyTest(SOP sop) throws IOException { + super.extractAliceCertFromAliceKeyTest(sop); + } + + @Disabled("BC uses old CTBs causing mismatching byte arrays :/") + @Override + public void extractBobsCertFromBobsKeyTest(SOP sop) throws IOException { + super.extractBobsCertFromBobsKeyTest(sop); + } + + @Disabled("BC uses old CTBs causing mismatching byte arrays :/") + @Override + public void extractCarolsCertFromCarolsKeyTest(SOP sop) throws IOException { + super.extractCarolsCertFromCarolsKeyTest(sop); + } +} diff --git a/pgpainless-sop/src/test/java/sop/testsuite/pgpainless/operation/PGPainlessGenerateKeyTest.java b/pgpainless-sop/src/test/java/sop/testsuite/pgpainless/operation/PGPainlessGenerateKeyTest.java new file mode 100644 index 00000000..fe78bed0 --- /dev/null +++ b/pgpainless-sop/src/test/java/sop/testsuite/pgpainless/operation/PGPainlessGenerateKeyTest.java @@ -0,0 +1,11 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.testsuite.pgpainless.operation; + +import sop.testsuite.operation.GenerateKeyTest; + +public class PGPainlessGenerateKeyTest extends GenerateKeyTest { + +} diff --git a/pgpainless-sop/src/test/java/sop/testsuite/pgpainless/operation/PGPainlessInlineSignInlineDetachDetachedVerifyTest.java b/pgpainless-sop/src/test/java/sop/testsuite/pgpainless/operation/PGPainlessInlineSignInlineDetachDetachedVerifyTest.java new file mode 100644 index 00000000..20fdc262 --- /dev/null +++ b/pgpainless-sop/src/test/java/sop/testsuite/pgpainless/operation/PGPainlessInlineSignInlineDetachDetachedVerifyTest.java @@ -0,0 +1,12 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.testsuite.pgpainless.operation; + +import sop.testsuite.operation.InlineSignInlineDetachDetachedVerifyTest; + +public class PGPainlessInlineSignInlineDetachDetachedVerifyTest + extends InlineSignInlineDetachDetachedVerifyTest { + +} diff --git a/pgpainless-sop/src/test/java/sop/testsuite/pgpainless/operation/PGPainlessInlineSignInlineVerifyTest.java b/pgpainless-sop/src/test/java/sop/testsuite/pgpainless/operation/PGPainlessInlineSignInlineVerifyTest.java new file mode 100644 index 00000000..16166eb1 --- /dev/null +++ b/pgpainless-sop/src/test/java/sop/testsuite/pgpainless/operation/PGPainlessInlineSignInlineVerifyTest.java @@ -0,0 +1,11 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.testsuite.pgpainless.operation; + +import sop.testsuite.operation.InlineSignInlineVerifyTest; + +public class PGPainlessInlineSignInlineVerifyTest extends InlineSignInlineVerifyTest { + +} diff --git a/pgpainless-sop/src/test/java/sop/testsuite/pgpainless/operation/PGPainlessVersionTest.java b/pgpainless-sop/src/test/java/sop/testsuite/pgpainless/operation/PGPainlessVersionTest.java new file mode 100644 index 00000000..7a8f7db4 --- /dev/null +++ b/pgpainless-sop/src/test/java/sop/testsuite/pgpainless/operation/PGPainlessVersionTest.java @@ -0,0 +1,11 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.testsuite.pgpainless.operation; + +import sop.testsuite.operation.VersionTest; + +public class PGPainlessVersionTest extends VersionTest { + +} From 9509fff92913af662d13c94a6aad4d18404e06d8 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 31 Jan 2023 19:10:14 +0100 Subject: [PATCH 229/763] Update changelog --- CHANGELOG.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 62740014..d730daed 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,13 @@ SPDX-License-Identifier: CC0-1.0 # PGPainless Changelog +## 1.4.3-SNAPSHOT +- Bump `sop-java` to `4.1.1` +- Reuse shared test suite of `sop-java` +- Add `EncryptionOptions.hasEncryptionMethod()` +- SOP `encrypt`: Throw `MissingArg` exception if no encryption method was provided +- Fix redundant dot in exception message (thanks @DenBond7) + ## 1.4.2 - Properly decrypt messages without MDC packets when `ConsumerOptions.setIgnoreMDCErrors(true)` is set - Fix crash in `sop generate-key --with-key-password` when more than one user-id is given From 1257c52ede623fd377a594ab4daa09ea5647ea1b Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 31 Jan 2023 19:12:45 +0100 Subject: [PATCH 230/763] PGPainless 1.4.3 --- CHANGELOG.md | 2 +- README.md | 2 +- pgpainless-sop/README.md | 4 ++-- version.gradle | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d730daed..d3475dd5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ SPDX-License-Identifier: CC0-1.0 # PGPainless Changelog -## 1.4.3-SNAPSHOT +## 1.4.3 - Bump `sop-java` to `4.1.1` - Reuse shared test suite of `sop-java` - Add `EncryptionOptions.hasEncryptionMethod()` diff --git a/README.md b/README.md index 3b6dc7cb..bc3484b8 100644 --- a/README.md +++ b/README.md @@ -191,7 +191,7 @@ repositories { } dependencies { - implementation 'org.pgpainless:pgpainless-core:1.4.2' + implementation 'org.pgpainless:pgpainless-core:1.4.3' } ``` diff --git a/pgpainless-sop/README.md b/pgpainless-sop/README.md index 97b7765a..74772110 100644 --- a/pgpainless-sop/README.md +++ b/pgpainless-sop/README.md @@ -23,7 +23,7 @@ To start using pgpainless-sop in your code, include the following lines in your ... dependencies { ... - implementation "org.pgpainless:pgpainless-sop:1.4.2" + implementation "org.pgpainless:pgpainless-sop:1.4.3" ... } @@ -34,7 +34,7 @@ dependencies { org.pgpainless pgpainless-sop - 1.4.2 + 1.4.3 ... diff --git a/version.gradle b/version.gradle index 7154bca5..914de80f 100644 --- a/version.gradle +++ b/version.gradle @@ -5,7 +5,7 @@ allprojects { ext { shortVersion = '1.4.3' - isSnapshot = true + isSnapshot = false pgpainlessMinAndroidSdk = 10 javaSourceCompatibility = 1.8 bouncyCastleVersion = '1.72' From 6c2331d4e61342042fefb7c495755c119976c4c0 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 31 Jan 2023 19:15:31 +0100 Subject: [PATCH 231/763] PGPainless 1.4.4-SNAPSHOT --- version.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/version.gradle b/version.gradle index 914de80f..68d74212 100644 --- a/version.gradle +++ b/version.gradle @@ -4,8 +4,8 @@ allprojects { ext { - shortVersion = '1.4.3' - isSnapshot = false + shortVersion = '1.4.4' + isSnapshot = true pgpainlessMinAndroidSdk = 10 javaSourceCompatibility = 1.8 bouncyCastleVersion = '1.72' From 30771f470a31e6fc094dd6824eb85cdfd1cc9cc0 Mon Sep 17 00:00:00 2001 From: Bastien JANSEN Date: Wed, 8 Feb 2023 09:16:53 +0100 Subject: [PATCH 232/763] Support version 3 signature packets --- .../consumer/SignatureValidator.java | 10 ++- .../subpackets/SignatureSubpacketsUtil.java | 3 + .../VerifyVersion3SignaturePacketTest.java | 65 +++++++++++++++++++ 3 files changed, 75 insertions(+), 3 deletions(-) create mode 100644 pgpainless-core/src/test/java/org/pgpainless/decryption_verification/VerifyVersion3SignaturePacketTest.java diff --git a/pgpainless-core/src/main/java/org/pgpainless/signature/consumer/SignatureValidator.java b/pgpainless-core/src/main/java/org/pgpainless/signature/consumer/SignatureValidator.java index af245235..cf0dc1fb 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/signature/consumer/SignatureValidator.java +++ b/pgpainless-core/src/main/java/org/pgpainless/signature/consumer/SignatureValidator.java @@ -158,8 +158,10 @@ public abstract class SignatureValidator { @Override public void verify(PGPSignature signature) throws SignatureValidationException { signatureIsNotMalformed(signingKey).verify(signature); - signatureDoesNotHaveCriticalUnknownNotations(policy.getNotationRegistry()).verify(signature); - signatureDoesNotHaveCriticalUnknownSubpackets().verify(signature); + if (signature.getVersion() >= 4) { + signatureDoesNotHaveCriticalUnknownNotations(policy.getNotationRegistry()).verify(signature); + signatureDoesNotHaveCriticalUnknownSubpackets().verify(signature); + } signatureUsesAcceptableHashAlgorithm(policy).verify(signature); signatureUsesAcceptablePublicKeyAlgorithm(policy, signingKey).verify(signature); } @@ -373,7 +375,9 @@ public abstract class SignatureValidator { return new SignatureValidator() { @Override public void verify(PGPSignature signature) throws SignatureValidationException { - signatureHasHashedCreationTime().verify(signature); + if (signature.getVersion() >= 4) { + signatureHasHashedCreationTime().verify(signature); + } signatureDoesNotPredateSigningKey(creator).verify(signature); if (signature.getSignatureType() != SignatureType.PRIMARYKEY_BINDING.getCode()) { signatureDoesNotPredateSigningKeyBindingDate(creator).verify(signature); diff --git a/pgpainless-core/src/main/java/org/pgpainless/signature/subpackets/SignatureSubpacketsUtil.java b/pgpainless-core/src/main/java/org/pgpainless/signature/subpackets/SignatureSubpacketsUtil.java index cbcbeafc..1105a813 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/signature/subpackets/SignatureSubpacketsUtil.java +++ b/pgpainless-core/src/main/java/org/pgpainless/signature/subpackets/SignatureSubpacketsUtil.java @@ -141,6 +141,9 @@ public final class SignatureSubpacketsUtil { * @return signature creation time subpacket */ public static @Nullable SignatureCreationTime getSignatureCreationTime(PGPSignature signature) { + if (signature.getVersion() == 3) { + return new SignatureCreationTime(false, signature.getCreationTime()); + } return hashed(signature, SignatureSubpacket.signatureCreationTime); } diff --git a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/VerifyVersion3SignaturePacketTest.java b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/VerifyVersion3SignaturePacketTest.java new file mode 100644 index 00000000..54b9c94b --- /dev/null +++ b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/VerifyVersion3SignaturePacketTest.java @@ -0,0 +1,65 @@ +package org.pgpainless.decryption_verification; + +import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.PGPPrivateKey; +import org.bouncycastle.openpgp.PGPSecretKeyRing; +import org.bouncycastle.openpgp.PGPSignature; +import org.bouncycastle.openpgp.PGPV3SignatureGenerator; +import org.bouncycastle.openpgp.operator.PGPContentSignerBuilder; +import org.bouncycastle.util.io.Streams; +import org.junit.jupiter.api.Test; +import org.pgpainless.PGPainless; +import org.pgpainless.algorithm.HashAlgorithm; +import org.pgpainless.algorithm.PublicKeyAlgorithm; +import org.pgpainless.algorithm.SignatureType; +import org.pgpainless.implementation.ImplementationFactory; +import org.pgpainless.key.TestKeys; +import org.pgpainless.key.protection.SecretKeyRingProtector; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +class VerifyVersion3SignaturePacketTest { + + + protected static final byte[] DATA = "hello".getBytes(StandardCharsets.UTF_8); + + @Test + void verifyDetachedVersion3Signature() throws PGPException, IOException { + PGPSignature version3Signature = generateV3Signature(); + + ConsumerOptions options = new ConsumerOptions() + .addVerificationCert(TestKeys.getEmilPublicKeyRing()) + .addVerificationOfDetachedSignatures(new ByteArrayInputStream(version3Signature.getEncoded())); + + DecryptionStream verifier = PGPainless.decryptAndOrVerify() + .onInputStream(new ByteArrayInputStream(DATA)) + .withOptions(options); + + OpenPgpMetadata metadata = processSignedData(verifier); + assertTrue(metadata.containsVerifiedSignatureFrom(TestKeys.getEmilPublicKeyRing())); + } + + private static PGPSignature generateV3Signature() throws IOException, PGPException { + PGPContentSignerBuilder builder = ImplementationFactory.getInstance().getPGPContentSignerBuilder(PublicKeyAlgorithm.ECDSA, HashAlgorithm.SHA512); + PGPV3SignatureGenerator signatureGenerator = new PGPV3SignatureGenerator(builder); + + PGPSecretKeyRing secretKeys = TestKeys.getEmilSecretKeyRing(); + SecretKeyRingProtector protector = SecretKeyRingProtector.unprotectedKeys(); + PGPPrivateKey privateKey = secretKeys.getSecretKey().extractPrivateKey(protector.getDecryptor(secretKeys.getSecretKey().getKeyID())); + + signatureGenerator.init(SignatureType.CANONICAL_TEXT_DOCUMENT.getCode(), privateKey); + signatureGenerator.update(DATA); + + return signatureGenerator.generate(); + } + + private OpenPgpMetadata processSignedData(DecryptionStream verifier) throws IOException { + Streams.drain(verifier); + verifier.close(); + return verifier.getResult(); + } +} From d03f84f415ff43716303e810b7dd8c700559392c Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 8 Feb 2023 14:49:10 +0100 Subject: [PATCH 233/763] Add reuse header to VerifyVersion3SignaturePacketTest --- .../VerifyVersion3SignaturePacketTest.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/VerifyVersion3SignaturePacketTest.java b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/VerifyVersion3SignaturePacketTest.java index 54b9c94b..2a12e74a 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/VerifyVersion3SignaturePacketTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/VerifyVersion3SignaturePacketTest.java @@ -1,3 +1,7 @@ +// SPDX-FileCopyrightText: 2023 Bastien Jansen +// +// SPDX-License-Identifier: Apache-2.0 + package org.pgpainless.decryption_verification; import org.bouncycastle.openpgp.PGPException; From 997a6c8c5d109b11585c9ff065afa9e63c11cbd0 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 8 Feb 2023 14:51:44 +0100 Subject: [PATCH 234/763] Update changelog --- CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d3475dd5..867980ea 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,11 @@ SPDX-License-Identifier: CC0-1.0 # PGPainless Changelog +## 1.4.4-SNAPSHOT +- Fix expectations on subpackets of v3 signatures (thanks @bjansen) + - Properly verify v3 signatures, which do not yet have signature subpackets, yet we required them to have + a hashed creation date subpacket. + ## 1.4.3 - Bump `sop-java` to `4.1.1` - Reuse shared test suite of `sop-java` From afc7d491445d641429474feb633f26effad2d348 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 9 Feb 2023 21:55:10 +0100 Subject: [PATCH 235/763] PGPainless 1.4.4 --- CHANGELOG.md | 2 +- README.md | 2 +- pgpainless-sop/README.md | 4 ++-- version.gradle | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 867980ea..bd7f3505 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ SPDX-License-Identifier: CC0-1.0 # PGPainless Changelog -## 1.4.4-SNAPSHOT +## 1.4.4 - Fix expectations on subpackets of v3 signatures (thanks @bjansen) - Properly verify v3 signatures, which do not yet have signature subpackets, yet we required them to have a hashed creation date subpacket. diff --git a/README.md b/README.md index bc3484b8..2a91789e 100644 --- a/README.md +++ b/README.md @@ -191,7 +191,7 @@ repositories { } dependencies { - implementation 'org.pgpainless:pgpainless-core:1.4.3' + implementation 'org.pgpainless:pgpainless-core:1.4.4' } ``` diff --git a/pgpainless-sop/README.md b/pgpainless-sop/README.md index 74772110..3aef6d05 100644 --- a/pgpainless-sop/README.md +++ b/pgpainless-sop/README.md @@ -23,7 +23,7 @@ To start using pgpainless-sop in your code, include the following lines in your ... dependencies { ... - implementation "org.pgpainless:pgpainless-sop:1.4.3" + implementation "org.pgpainless:pgpainless-sop:1.4.4" ... } @@ -34,7 +34,7 @@ dependencies { org.pgpainless pgpainless-sop - 1.4.3 + 1.4.4 ... diff --git a/version.gradle b/version.gradle index 68d74212..2593724d 100644 --- a/version.gradle +++ b/version.gradle @@ -5,7 +5,7 @@ allprojects { ext { shortVersion = '1.4.4' - isSnapshot = true + isSnapshot = false pgpainlessMinAndroidSdk = 10 javaSourceCompatibility = 1.8 bouncyCastleVersion = '1.72' From a25ea542d69651fa0dfbccf0d3cb1151c83f772a Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 9 Feb 2023 21:58:37 +0100 Subject: [PATCH 236/763] PGPainless 1.4.5-SNAPSHOT --- version.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/version.gradle b/version.gradle index 2593724d..55e9b151 100644 --- a/version.gradle +++ b/version.gradle @@ -4,8 +4,8 @@ allprojects { ext { - shortVersion = '1.4.4' - isSnapshot = false + shortVersion = '1.4.5' + isSnapshot = true pgpainlessMinAndroidSdk = 10 javaSourceCompatibility = 1.8 bouncyCastleVersion = '1.72' From ed2c53f5d6362afec315e43a84cb35c5745ccd55 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Sat, 25 Feb 2023 11:26:58 +0100 Subject: [PATCH 237/763] Make getLastModified() @Nonnull --- .../src/main/java/org/pgpainless/key/info/KeyRingInfo.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/info/KeyRingInfo.java b/pgpainless-core/src/main/java/org/pgpainless/key/info/KeyRingInfo.java index fa4168dd..46dd500b 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/key/info/KeyRingInfo.java +++ b/pgpainless-core/src/main/java/org/pgpainless/key/info/KeyRingInfo.java @@ -611,7 +611,7 @@ public class KeyRingInfo { * * @return last modification date. */ - public @Nullable Date getLastModified() { + public @Nonnull Date getLastModified() { PGPSignature mostRecent = getMostRecentSignature(); if (mostRecent == null) { // No sigs found. Return public key creation date instead. From acb5d3fd9e98041948e6742804b3c1c439f124e1 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Fri, 7 Apr 2023 11:26:38 +0200 Subject: [PATCH 238/763] getEncryptionSubkeys(): Compare expirations against reference date --- .../src/main/java/org/pgpainless/key/info/KeyRingInfo.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/info/KeyRingInfo.java b/pgpainless-core/src/main/java/org/pgpainless/key/info/KeyRingInfo.java index 46dd500b..1ebd023e 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/key/info/KeyRingInfo.java +++ b/pgpainless-core/src/main/java/org/pgpainless/key/info/KeyRingInfo.java @@ -903,7 +903,7 @@ public class KeyRingInfo { */ public @Nonnull List getEncryptionSubkeys(EncryptionPurpose purpose) { Date primaryExpiration = getPrimaryKeyExpirationDate(); - if (primaryExpiration != null && primaryExpiration.before(new Date())) { + if (primaryExpiration != null && primaryExpiration.before(referenceDate)) { return Collections.emptyList(); } @@ -917,7 +917,7 @@ public class KeyRingInfo { } Date subkeyExpiration = getSubkeyExpirationDate(OpenPgpFingerprint.of(subKey)); - if (subkeyExpiration != null && subkeyExpiration.before(new Date())) { + if (subkeyExpiration != null && subkeyExpiration.before(referenceDate)) { continue; } From e744668f5ae63dec785b02e48d13f7bb5f8aedd6 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Fri, 7 Apr 2023 11:47:40 +0200 Subject: [PATCH 239/763] Deprecate OpenPgpFingerprint.parse() methods --- .../src/main/java/org/pgpainless/key/OpenPgpFingerprint.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/OpenPgpFingerprint.java b/pgpainless-core/src/main/java/org/pgpainless/key/OpenPgpFingerprint.java index 86ac8265..0525f4c9 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/key/OpenPgpFingerprint.java +++ b/pgpainless-core/src/main/java/org/pgpainless/key/OpenPgpFingerprint.java @@ -55,7 +55,9 @@ public abstract class OpenPgpFingerprint implements CharSequence, Comparable Date: Fri, 7 Apr 2023 12:28:27 +0200 Subject: [PATCH 240/763] Introduce OpenPgpv6Fingerprint --- .../pgpainless/key/OpenPgpFingerprint.java | 11 +- .../pgpainless/key/OpenPgpV5Fingerprint.java | 67 +------- .../pgpainless/key/OpenPgpV6Fingerprint.java | 58 +++++++ .../pgpainless/key/_64DigitFingerprint.java | 119 ++++++++++++++ .../key/OpenPgpV5FingerprintTest.java | 69 +------- .../key/OpenPgpV6FingerprintTest.java | 154 ++++++++++++++++++ .../key/_64DigitFingerprintTest.java | 85 ++++++++++ 7 files changed, 430 insertions(+), 133 deletions(-) create mode 100644 pgpainless-core/src/main/java/org/pgpainless/key/OpenPgpV6Fingerprint.java create mode 100644 pgpainless-core/src/main/java/org/pgpainless/key/_64DigitFingerprint.java create mode 100644 pgpainless-core/src/test/java/org/pgpainless/key/OpenPgpV6FingerprintTest.java create mode 100644 pgpainless-core/src/test/java/org/pgpainless/key/_64DigitFingerprintTest.java diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/OpenPgpFingerprint.java b/pgpainless-core/src/main/java/org/pgpainless/key/OpenPgpFingerprint.java index 0525f4c9..1ac900a0 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/key/OpenPgpFingerprint.java +++ b/pgpainless-core/src/main/java/org/pgpainless/key/OpenPgpFingerprint.java @@ -36,6 +36,9 @@ public abstract class OpenPgpFingerprint implements CharSequence, Comparable +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.key; + +import javax.annotation.Nonnull; + +import org.bouncycastle.openpgp.PGPKeyRing; +import org.bouncycastle.openpgp.PGPPublicKey; +import org.bouncycastle.openpgp.PGPPublicKeyRing; +import org.bouncycastle.openpgp.PGPSecretKey; +import org.bouncycastle.openpgp.PGPSecretKeyRing; + +/** + * This class represents a hex encoded, upper case OpenPGP v6 fingerprint. + */ +public class OpenPgpV6Fingerprint extends _64DigitFingerprint { + + /** + * Create an {@link OpenPgpV6Fingerprint}. + * + * @param fingerprint uppercase hexadecimal fingerprint of length 64 + */ + public OpenPgpV6Fingerprint(@Nonnull String fingerprint) { + super(fingerprint); + } + + public OpenPgpV6Fingerprint(@Nonnull byte[] bytes) { + super(bytes); + } + + public OpenPgpV6Fingerprint(@Nonnull PGPPublicKey key) { + super(key); + } + + public OpenPgpV6Fingerprint(@Nonnull PGPSecretKey key) { + this(key.getPublicKey()); + } + + public OpenPgpV6Fingerprint(@Nonnull PGPPublicKeyRing ring) { + super(ring); + } + + public OpenPgpV6Fingerprint(@Nonnull PGPSecretKeyRing ring) { + super(ring); + } + + public OpenPgpV6Fingerprint(@Nonnull PGPKeyRing ring) { + super(ring); + } + + @Override + public int getVersion() { + return 6; + } + +} diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/_64DigitFingerprint.java b/pgpainless-core/src/main/java/org/pgpainless/key/_64DigitFingerprint.java new file mode 100644 index 00000000..11f18058 --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/key/_64DigitFingerprint.java @@ -0,0 +1,119 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.key; + +import java.nio.Buffer; +import java.nio.ByteBuffer; +import javax.annotation.Nonnull; + +import org.bouncycastle.openpgp.PGPKeyRing; +import org.bouncycastle.openpgp.PGPPublicKey; +import org.bouncycastle.openpgp.PGPPublicKeyRing; +import org.bouncycastle.openpgp.PGPSecretKey; +import org.bouncycastle.openpgp.PGPSecretKeyRing; +import org.bouncycastle.util.encoders.Hex; + +/** + * This class represents a hex encoded, upper case OpenPGP v5 or v6 fingerprint. + * Since both fingerprints use the same format, this class is used when parsing the fingerprint without knowing the + * key version. + */ +public class _64DigitFingerprint extends OpenPgpFingerprint { + + /** + * Create an {@link _64DigitFingerprint}. + * + * @param fingerprint uppercase hexadecimal fingerprint of length 64 + */ + protected _64DigitFingerprint(@Nonnull String fingerprint) { + super(fingerprint); + } + + protected _64DigitFingerprint(@Nonnull byte[] bytes) { + super(Hex.encode(bytes)); + } + + protected _64DigitFingerprint(@Nonnull PGPPublicKey key) { + super(key); + } + + protected _64DigitFingerprint(@Nonnull PGPSecretKey key) { + this(key.getPublicKey()); + } + + protected _64DigitFingerprint(@Nonnull PGPPublicKeyRing ring) { + super(ring); + } + + protected _64DigitFingerprint(@Nonnull PGPSecretKeyRing ring) { + super(ring); + } + + protected _64DigitFingerprint(@Nonnull PGPKeyRing ring) { + super(ring); + } + + @Override + public int getVersion() { + return -1; // might be v5 or v6 + } + + @Override + protected boolean isValid(@Nonnull String fp) { + return fp.matches("^[0-9A-F]{64}$"); + } + + @Override + public long getKeyId() { + byte[] bytes = Hex.decode(toString().getBytes(utf8)); + ByteBuffer buf = ByteBuffer.wrap(bytes); + + // The key id is the left-most 8 bytes (conveniently a long). + // We have to cast here in order to be compatible with java 8 + // https://github.com/eclipse/jetty.project/issues/3244 + ((Buffer) buf).position(0); + + return buf.getLong(); + } + + @Override + public String prettyPrint() { + String fp = toString(); + StringBuilder pretty = new StringBuilder(); + + for (int i = 0; i < 4; i++) { + pretty.append(fp, i * 8, (i + 1) * 8).append(' '); + } + pretty.append(' '); + for (int i = 4; i < 7; i++) { + pretty.append(fp, i * 8, (i + 1) * 8).append(' '); + } + pretty.append(fp, 56, 64); + return pretty.toString(); + } + + @Override + public boolean equals(Object other) { + if (other == null) { + return false; + } + + if (!(other instanceof CharSequence)) { + return false; + } + + return this.toString().equals(other.toString()); + } + + @Override + public int hashCode() { + return toString().hashCode(); + } + + @Override + public int compareTo(OpenPgpFingerprint openPgpFingerprint) { + return toString().compareTo(openPgpFingerprint.toString()); + } +} diff --git a/pgpainless-core/src/test/java/org/pgpainless/key/OpenPgpV5FingerprintTest.java b/pgpainless-core/src/test/java/org/pgpainless/key/OpenPgpV5FingerprintTest.java index a250bef4..57c98928 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/key/OpenPgpV5FingerprintTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/key/OpenPgpV5FingerprintTest.java @@ -5,8 +5,6 @@ package org.pgpainless.key; import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotEquals; -import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -29,77 +27,12 @@ public class OpenPgpV5FingerprintTest { OpenPgpV5Fingerprint fingerprint = new OpenPgpV5Fingerprint(fp); assertEquals(fp, fingerprint.toString()); assertEquals(pretty, fingerprint.prettyPrint()); + assertEquals(5, fingerprint.getVersion()); long id = fingerprint.getKeyId(); assertEquals("76543210abcdefab", Long.toHexString(id)); } - @Test - public void testParse() { - String prettyPrint = "76543210 ABCDEFAB 01AB23CD 1C0FFEE1 1EEFF0C1 DC32BA10 BAFEDCBA 01234567"; - OpenPgpFingerprint parsed = OpenPgpFingerprint.parse(prettyPrint); - - assertTrue(parsed instanceof OpenPgpV5Fingerprint); - OpenPgpV5Fingerprint v5fp = (OpenPgpV5Fingerprint) parsed; - assertEquals(prettyPrint, v5fp.prettyPrint()); - assertEquals(5, v5fp.getVersion()); - } - - @Test - public void testParseFromBinary() { - String hex = "76543210ABCDEFAB01AB23CD1C0FFEE11EEFF0C1DC32BA10BAFEDCBA01234567"; - byte[] binary = Hex.decode(hex); - - OpenPgpFingerprint fingerprint = OpenPgpFingerprint.parseFromBinary(binary); - assertTrue(fingerprint instanceof OpenPgpV5Fingerprint); - assertEquals(hex, fingerprint.toString()); - - OpenPgpV5Fingerprint constructed = new OpenPgpV5Fingerprint(binary); - assertEquals(fingerprint, constructed); - } - - @Test - public void testParseFromBinary_leadingZeros() { - String hex = "000000000000000001AB23CD1C0FFEE11EEFF0C1DC32BA10BAFEDCBA01234567"; - byte[] binary = Hex.decode(hex); - - OpenPgpFingerprint fingerprint = OpenPgpFingerprint.parseFromBinary(binary); - assertTrue(fingerprint instanceof OpenPgpV5Fingerprint); - assertEquals(hex, fingerprint.toString()); - } - - @Test - public void testParseFromBinary_trailingZeros() { - String hex = "76543210ABCDEFAB01AB23CD1C0FFEE11EEFF0C1DC32BA100000000000000000"; - byte[] binary = Hex.decode(hex); - - OpenPgpFingerprint fingerprint = OpenPgpFingerprint.parseFromBinary(binary); - assertTrue(fingerprint instanceof OpenPgpV5Fingerprint); - assertEquals(hex, fingerprint.toString()); - } - - @Test - public void testParseFromBinary_wrongLength() { - String hex = "76543210ABCDEFAB01AB23CD1C0FFEE11EEFF0C1DC32BA10BAFEDCBA012345"; // missing 2 digits - byte[] binary = Hex.decode(hex); - - assertThrows(IllegalArgumentException.class, () -> OpenPgpFingerprint.parseFromBinary(binary)); - } - - @Test - public void equalsTest() { - String prettyPrint = "76543210 ABCDEFAB 01AB23CD 1C0FFEE1 1EEFF0C1 DC32BA10 BAFEDCBA 01234567"; - OpenPgpFingerprint parsed = OpenPgpFingerprint.parse(prettyPrint); - - assertNotEquals(parsed, null); - assertNotEquals(parsed, new Object()); - assertEquals(parsed, parsed.toString()); - - OpenPgpFingerprint parsed2 = new OpenPgpV5Fingerprint(prettyPrint); - assertEquals(parsed.hashCode(), parsed2.hashCode()); - assertEquals(0, parsed.compareTo(parsed2)); - } - @Test public void constructFromMockedPublicKey() { String hex = "76543210ABCDEFAB01AB23CD1C0FFEE11EEFF0C1DC32BA10BAFEDCBA01234567"; diff --git a/pgpainless-core/src/test/java/org/pgpainless/key/OpenPgpV6FingerprintTest.java b/pgpainless-core/src/test/java/org/pgpainless/key/OpenPgpV6FingerprintTest.java new file mode 100644 index 00000000..7a7b0e45 --- /dev/null +++ b/pgpainless-core/src/test/java/org/pgpainless/key/OpenPgpV6FingerprintTest.java @@ -0,0 +1,154 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.key; + +import org.bouncycastle.openpgp.PGPKeyRing; +import org.bouncycastle.openpgp.PGPPublicKey; +import org.bouncycastle.openpgp.PGPPublicKeyRing; +import org.bouncycastle.openpgp.PGPSecretKey; +import org.bouncycastle.openpgp.PGPSecretKeyRing; +import org.bouncycastle.util.encoders.Hex; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class OpenPgpV6FingerprintTest { + + @Test + public void testFingerprintFormatting() { + String pretty = "76543210 ABCDEFAB 01AB23CD 1C0FFEE1 1EEFF0C1 DC32BA10 BAFEDCBA 01234567"; + String fp = pretty.replace(" ", ""); + + OpenPgpV6Fingerprint fingerprint = new OpenPgpV6Fingerprint(fp); + assertEquals(fp, fingerprint.toString()); + assertEquals(pretty, fingerprint.prettyPrint()); + assertEquals(6, fingerprint.getVersion()); + + long id = fingerprint.getKeyId(); + assertEquals("76543210abcdefab", Long.toHexString(id)); + } + + @Test + public void testParseFromBinary_leadingZeros() { + String hex = "000000000000000001AB23CD1C0FFEE11EEFF0C1DC32BA10BAFEDCBA01234567"; + byte[] binary = Hex.decode(hex); + + OpenPgpFingerprint fingerprint = new OpenPgpV6Fingerprint(binary); + assertEquals(hex, fingerprint.toString()); + } + + @Test + public void testParseFromBinary_trailingZeros() { + String hex = "76543210ABCDEFAB01AB23CD1C0FFEE11EEFF0C1DC32BA100000000000000000"; + byte[] binary = Hex.decode(hex); + + OpenPgpFingerprint fingerprint = new OpenPgpV6Fingerprint(binary); + assertEquals(hex, fingerprint.toString()); + } + + @Test + public void testParseFromBinary_wrongLength() { + String hex = "76543210ABCDEFAB01AB23CD1C0FFEE11EEFF0C1DC32BA10BAFEDCBA012345"; // missing 2 digits + byte[] binary = Hex.decode(hex); + + assertThrows(IllegalArgumentException.class, () -> new OpenPgpV6Fingerprint(binary)); + } + + @Test + public void equalsTest() { + String prettyPrint = "76543210 ABCDEFAB 01AB23CD 1C0FFEE1 1EEFF0C1 DC32BA10 BAFEDCBA 01234567"; + OpenPgpFingerprint parsed = new OpenPgpV6Fingerprint(prettyPrint); + + assertNotEquals(parsed, null); + assertNotEquals(parsed, new Object()); + assertEquals(parsed, parsed.toString()); + + OpenPgpFingerprint parsed2 = new OpenPgpV6Fingerprint(prettyPrint); + assertEquals(parsed.hashCode(), parsed2.hashCode()); + assertEquals(0, parsed.compareTo(parsed2)); + } + + @Test + public void constructFromMockedPublicKey() { + String hex = "76543210ABCDEFAB01AB23CD1C0FFEE11EEFF0C1DC32BA10BAFEDCBA01234567"; + PGPPublicKey publicKey = getMockedPublicKey(hex); + + OpenPgpFingerprint fingerprint = OpenPgpFingerprint.of(publicKey); + assertTrue(fingerprint instanceof OpenPgpV6Fingerprint); + assertEquals(6, fingerprint.getVersion()); + assertEquals(hex, fingerprint.toString()); + } + + @Test + public void constructFromMockedSecretKey() { + String hex = "76543210ABCDEFAB01AB23CD1C0FFEE11EEFF0C1DC32BA10BAFEDCBA01234567"; + PGPPublicKey publicKey = getMockedPublicKey(hex); + PGPSecretKey secretKey = mock(PGPSecretKey.class); + when(secretKey.getPublicKey()).thenReturn(publicKey); + + OpenPgpFingerprint fingerprint = new OpenPgpV6Fingerprint(secretKey); + assertEquals(6, fingerprint.getVersion()); + assertEquals(hex, fingerprint.toString()); + } + + @Test + public void constructFromMockedPublicKeyRing() { + String hex = "76543210ABCDEFAB01AB23CD1C0FFEE11EEFF0C1DC32BA10BAFEDCBA01234567"; + PGPPublicKey publicKey = getMockedPublicKey(hex); + PGPPublicKeyRing publicKeys = mock(PGPPublicKeyRing.class); + when(publicKeys.getPublicKey()).thenReturn(publicKey); + + OpenPgpFingerprint fingerprint = OpenPgpFingerprint.of(publicKeys); + assertEquals(6, fingerprint.getVersion()); + assertEquals(hex, fingerprint.toString()); + + fingerprint = new OpenPgpV6Fingerprint(publicKeys); + assertEquals(hex, fingerprint.toString()); + } + + @Test + public void constructFromMockedSecretKeyRing() { + String hex = "76543210ABCDEFAB01AB23CD1C0FFEE11EEFF0C1DC32BA10BAFEDCBA01234567"; + PGPPublicKey publicKey = getMockedPublicKey(hex); + PGPSecretKeyRing secretKeys = mock(PGPSecretKeyRing.class); + when(secretKeys.getPublicKey()).thenReturn(publicKey); + + OpenPgpFingerprint fingerprint = OpenPgpFingerprint.of(secretKeys); + assertEquals(6, fingerprint.getVersion()); + assertEquals(hex, fingerprint.toString()); + + fingerprint = new OpenPgpV6Fingerprint(secretKeys); + assertEquals(hex, fingerprint.toString()); + } + + @Test + public void constructFromMockedKeyRing() { + String hex = "76543210ABCDEFAB01AB23CD1C0FFEE11EEFF0C1DC32BA10BAFEDCBA01234567"; + PGPPublicKey publicKey = getMockedPublicKey(hex); + PGPKeyRing keys = mock(PGPKeyRing.class); + when(keys.getPublicKey()).thenReturn(publicKey); + + OpenPgpFingerprint fingerprint = OpenPgpFingerprint.of(keys); + assertEquals(6, fingerprint.getVersion()); + assertEquals(hex, fingerprint.toString()); + + fingerprint = new OpenPgpV6Fingerprint(keys); + assertEquals(hex, fingerprint.toString()); + } + + private PGPPublicKey getMockedPublicKey(String hex) { + byte[] binary = Hex.decode(hex); + + PGPPublicKey mocked = mock(PGPPublicKey.class); + when(mocked.getVersion()).thenReturn(6); + when(mocked.getFingerprint()).thenReturn(binary); + return mocked; + } +} diff --git a/pgpainless-core/src/test/java/org/pgpainless/key/_64DigitFingerprintTest.java b/pgpainless-core/src/test/java/org/pgpainless/key/_64DigitFingerprintTest.java new file mode 100644 index 00000000..21ff31c3 --- /dev/null +++ b/pgpainless-core/src/test/java/org/pgpainless/key/_64DigitFingerprintTest.java @@ -0,0 +1,85 @@ +package org.pgpainless.key; + +import org.bouncycastle.util.encoders.Hex; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class _64DigitFingerprintTest { + + @Test + public void testParse() { + String prettyPrint = "76543210 ABCDEFAB 01AB23CD 1C0FFEE1 1EEFF0C1 DC32BA10 BAFEDCBA 01234567"; + OpenPgpFingerprint parsed = OpenPgpFingerprint.parse(prettyPrint); + + assertTrue(parsed instanceof _64DigitFingerprint); + assertEquals(prettyPrint, parsed.prettyPrint()); + assertEquals(-1, parsed.getVersion()); + } + + @Test + public void testParseFromBinary() { + String hex = "76543210ABCDEFAB01AB23CD1C0FFEE11EEFF0C1DC32BA10BAFEDCBA01234567"; + byte[] binary = Hex.decode(hex); + + OpenPgpFingerprint fingerprint = OpenPgpFingerprint.parseFromBinary(binary); + assertTrue(fingerprint instanceof _64DigitFingerprint); + assertEquals(hex, fingerprint.toString()); + + OpenPgpV5Fingerprint v5 = new OpenPgpV5Fingerprint(binary); + assertEquals(fingerprint, v5); + + OpenPgpV6Fingerprint v6 = new OpenPgpV6Fingerprint(binary); + assertEquals(fingerprint, v6); + } + + @Test + public void testParseFromBinary_leadingZeros() { + String hex = "000000000000000001AB23CD1C0FFEE11EEFF0C1DC32BA10BAFEDCBA01234567"; + byte[] binary = Hex.decode(hex); + + OpenPgpFingerprint fingerprint = OpenPgpFingerprint.parseFromBinary(binary); + assertTrue(fingerprint instanceof _64DigitFingerprint); + assertEquals(hex, fingerprint.toString()); + } + + @Test + public void testParseFromBinary_trailingZeros() { + String hex = "76543210ABCDEFAB01AB23CD1C0FFEE11EEFF0C1DC32BA100000000000000000"; + byte[] binary = Hex.decode(hex); + + OpenPgpFingerprint fingerprint = OpenPgpFingerprint.parseFromBinary(binary); + assertTrue(fingerprint instanceof _64DigitFingerprint); + assertEquals(hex, fingerprint.toString()); + } + + @Test + public void testParseFromBinary_wrongLength() { + String hex = "76543210ABCDEFAB01AB23CD1C0FFEE11EEFF0C1DC32BA10BAFEDCBA012345"; // missing 2 digits + byte[] binary = Hex.decode(hex); + + assertThrows(IllegalArgumentException.class, () -> OpenPgpFingerprint.parseFromBinary(binary)); + } + + @Test + public void equalsTest() { + String prettyPrint = "76543210 ABCDEFAB 01AB23CD 1C0FFEE1 1EEFF0C1 DC32BA10 BAFEDCBA 01234567"; + OpenPgpFingerprint parsed = OpenPgpFingerprint.parse(prettyPrint); + + assertNotEquals(parsed, null); + assertNotEquals(parsed, new Object()); + assertEquals(parsed, parsed.toString()); + + OpenPgpFingerprint v5 = new OpenPgpV5Fingerprint(prettyPrint); + assertEquals(parsed.hashCode(), v5.hashCode()); + assertEquals(0, parsed.compareTo(v5)); + + OpenPgpFingerprint v6 = new OpenPgpV6Fingerprint(prettyPrint); + assertEquals(parsed.hashCode(), v6.hashCode()); + assertEquals(0, parsed.compareTo(v6)); + } + +} From 2587f19df345075b6c55949aee06dd08c346e156 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Sun, 9 Apr 2023 18:49:20 +0200 Subject: [PATCH 241/763] BC173: Fix CRC error detection by improving error check --- .../pgpainless/decryption_verification/TeeBCPGInputStream.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/TeeBCPGInputStream.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/TeeBCPGInputStream.java index 28b415e0..bdbd9bcd 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/TeeBCPGInputStream.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/TeeBCPGInputStream.java @@ -120,7 +120,7 @@ public class TeeBCPGInputStream { last = inputStream.read(); return last; } catch (IOException e) { - if ("crc check failed in armored message.".equals(e.getMessage())) { + if (e.getMessage().contains("crc check failed in armored message")) { throw e; } return -1; From 44608744c26f2df63daba7f3a8e24d706b8d5db0 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Fri, 14 Apr 2023 16:17:58 +0200 Subject: [PATCH 242/763] Add missing license header --- .../test/java/org/pgpainless/key/_64DigitFingerprintTest.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pgpainless-core/src/test/java/org/pgpainless/key/_64DigitFingerprintTest.java b/pgpainless-core/src/test/java/org/pgpainless/key/_64DigitFingerprintTest.java index 21ff31c3..a38fa61d 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/key/_64DigitFingerprintTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/key/_64DigitFingerprintTest.java @@ -1,3 +1,7 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + package org.pgpainless.key; import org.bouncycastle.util.encoders.Hex; From e35287a66658af3213628be09b6d025f401cb40c Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Fri, 14 Apr 2023 14:31:48 +0200 Subject: [PATCH 243/763] Add support for SOP05 features --- .../org/pgpainless/sop/GenerateKeyImpl.java | 48 ++++++++++++++++++- .../org/pgpainless/sop/ListProfilesImpl.java | 29 +++++++++++ .../main/java/org/pgpainless/sop/SOPImpl.java | 6 +++ version.gradle | 2 +- 4 files changed, 82 insertions(+), 3 deletions(-) create mode 100644 pgpainless-sop/src/main/java/org/pgpainless/sop/ListProfilesImpl.java diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/GenerateKeyImpl.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/GenerateKeyImpl.java index da99c854..693ca454 100644 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/GenerateKeyImpl.java +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/GenerateKeyImpl.java @@ -8,18 +8,22 @@ import java.io.IOException; import java.io.OutputStream; import java.security.InvalidAlgorithmParameterException; import java.security.NoSuchAlgorithmException; +import java.util.Arrays; import java.util.Iterator; import java.util.LinkedHashSet; +import java.util.List; import java.util.Set; import org.bouncycastle.bcpg.ArmoredOutputStream; import org.bouncycastle.openpgp.PGPException; import org.bouncycastle.openpgp.PGPSecretKeyRing; import org.pgpainless.PGPainless; +import org.pgpainless.key.generation.type.rsa.RsaLength; import org.pgpainless.key.modification.secretkeyring.SecretKeyRingEditorInterface; import org.pgpainless.key.protection.SecretKeyRingProtector; import org.pgpainless.util.ArmorUtils; import org.pgpainless.util.Passphrase; +import sop.Profile; import sop.Ready; import sop.exception.SOPGPException; import sop.operation.GenerateKey; @@ -29,9 +33,16 @@ import sop.operation.GenerateKey; */ public class GenerateKeyImpl implements GenerateKey { + public static final Profile DEFAULT_PROFILE = new Profile("default", "Generate keys based on XDH and EdDSA"); + public static final Profile RSA3072_PROFILE = new Profile("rfc4880-rsa3072@pgpainless.org", "Generate 3072-bit RSA keys"); + public static final Profile RSA4096_PROFILE = new Profile("rfc4880-rsa4096@pgpainless.org", "Generate 4096-bit RSA keys"); + + public static final List SUPPORTED_PROFILES = Arrays.asList(DEFAULT_PROFILE, RSA3072_PROFILE, RSA4096_PROFILE); + private boolean armor = true; private final Set userIds = new LinkedHashSet<>(); private Passphrase passphrase = Passphrase.emptyPassphrase(); + private String profile = DEFAULT_PROFILE.getName(); @Override public GenerateKey noArmor() { @@ -51,6 +62,18 @@ public class GenerateKeyImpl implements GenerateKey { return this; } + @Override + public GenerateKey profile(String profileName) { + for (Profile profile : SUPPORTED_PROFILES) { + if (profile.getName().equals(profileName)) { + this.profile = profileName; + return this; + } + } + + throw new SOPGPException.UnsupportedProfile("generate-key", profileName); + } + @Override public Ready generate() throws SOPGPException.MissingArg, SOPGPException.UnsupportedAsymmetricAlgo { Iterator userIdIterator = userIds.iterator(); @@ -58,8 +81,7 @@ public class GenerateKeyImpl implements GenerateKey { PGPSecretKeyRing key; try { String primaryUserId = userIdIterator.hasNext() ? userIdIterator.next() : null; - key = PGPainless.generateKeyRing() - .modernKeyRing(primaryUserId, passphrase); + key = generateKeyWithProfile(profile, primaryUserId, passphrase); if (userIdIterator.hasNext()) { SecretKeyRingEditorInterface editor = PGPainless.modifyKeyRing(key); @@ -90,4 +112,26 @@ public class GenerateKeyImpl implements GenerateKey { throw new RuntimeException(e); } } + + private PGPSecretKeyRing generateKeyWithProfile(String profile, String primaryUserId, Passphrase passphrase) + throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException { + PGPSecretKeyRing key; + // XDH + EdDSA + if (profile.equals(DEFAULT_PROFILE.getName())) { + key = PGPainless.generateKeyRing() + .modernKeyRing(primaryUserId, passphrase); + } + else if (profile.equals(RSA3072_PROFILE.getName())) { + key = PGPainless.generateKeyRing() + .simpleRsaKeyRing(primaryUserId, RsaLength._3072, passphrase); + } + else if (profile.equals(RSA4096_PROFILE.getName())) { + key = PGPainless.generateKeyRing() + .simpleRsaKeyRing(primaryUserId, RsaLength._4096, passphrase); + } + else { + throw new SOPGPException.UnsupportedProfile("generate-key", profile); + } + return key; + } } diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/ListProfilesImpl.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/ListProfilesImpl.java new file mode 100644 index 00000000..06519bb3 --- /dev/null +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/ListProfilesImpl.java @@ -0,0 +1,29 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.sop; + +import java.util.List; + +import sop.Profile; +import sop.exception.SOPGPException; +import sop.operation.ListProfiles; + +public class ListProfilesImpl implements ListProfiles { + + @Override + public List subcommand(String command) { + if (command == null) { + throw new SOPGPException.UnsupportedProfile("null"); + } + + switch (command) { + case "generate-key": + return GenerateKeyImpl.SUPPORTED_PROFILES; + + default: + throw new SOPGPException.UnsupportedProfile(command); + } + } +} diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/SOPImpl.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/SOPImpl.java index a49f7e34..a0e5f631 100644 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/SOPImpl.java +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/SOPImpl.java @@ -17,6 +17,7 @@ import sop.operation.ExtractCert; import sop.operation.GenerateKey; import sop.operation.InlineSign; import sop.operation.InlineVerify; +import sop.operation.ListProfiles; import sop.operation.Version; /** @@ -96,6 +97,11 @@ public class SOPImpl implements SOP { return new DearmorImpl(); } + @Override + public ListProfiles listProfiles() { + return new ListProfilesImpl(); + } + @Override public InlineDetach inlineDetach() { return new InlineDetachImpl(); diff --git a/version.gradle b/version.gradle index 55e9b151..1adf151a 100644 --- a/version.gradle +++ b/version.gradle @@ -18,6 +18,6 @@ allprojects { logbackVersion = '1.2.11' mockitoVersion = '4.5.1' slf4jVersion = '1.7.36' - sopJavaVersion = '4.1.1' + sopJavaVersion = '5.0.0-SNAPSHOT' } } From b79e706d65c33defebd132e9ec3db5f6725c89db Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Fri, 14 Apr 2023 14:35:55 +0200 Subject: [PATCH 244/763] Bump SOP version in VersionImpl to 05 --- .../src/main/java/org/pgpainless/sop/VersionImpl.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/VersionImpl.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/VersionImpl.java index 4449af10..13a5dc94 100644 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/VersionImpl.java +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/VersionImpl.java @@ -18,7 +18,7 @@ import sop.operation.Version; public class VersionImpl implements Version { // draft version - private static final String SOP_VERSION = "04"; + private static final String SOP_VERSION = "05"; @Override public String getName() { From f3a4a01d199bd3782d2e34b065ac16c47dd5253b Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Fri, 14 Apr 2023 16:17:32 +0200 Subject: [PATCH 245/763] Add basic tests for new functionality --- .../org/pgpainless/sop/GenerateKeyTest.java | 14 ++++++++ .../org/pgpainless/sop/ListProfilesTest.java | 34 +++++++++++++++++++ 2 files changed, 48 insertions(+) create mode 100644 pgpainless-sop/src/test/java/org/pgpainless/sop/ListProfilesTest.java diff --git a/pgpainless-sop/src/test/java/org/pgpainless/sop/GenerateKeyTest.java b/pgpainless-sop/src/test/java/org/pgpainless/sop/GenerateKeyTest.java index 3a6e4476..5894bfa7 100644 --- a/pgpainless-sop/src/test/java/org/pgpainless/sop/GenerateKeyTest.java +++ b/pgpainless-sop/src/test/java/org/pgpainless/sop/GenerateKeyTest.java @@ -7,6 +7,7 @@ package org.pgpainless.sop; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import java.io.IOException; @@ -21,6 +22,7 @@ import org.pgpainless.key.info.KeyRingInfo; import org.pgpainless.key.protection.UnlockSecretKey; import org.pgpainless.util.Passphrase; import sop.SOP; +import sop.exception.SOPGPException; public class GenerateKeyTest { @@ -92,4 +94,16 @@ public class GenerateKeyTest { assertNotNull(UnlockSecretKey.unlockSecretKey(key, Passphrase.fromPassword("sw0rdf1sh"))); } } + + @Test + public void invalidProfile() { + assertThrows(SOPGPException.UnsupportedProfile.class, () -> + sop.generateKey().profile("invalid")); + } + + @Test + public void nullProfile() { + assertThrows(SOPGPException.UnsupportedProfile.class, () -> + sop.generateKey().profile((String) null)); + } } diff --git a/pgpainless-sop/src/test/java/org/pgpainless/sop/ListProfilesTest.java b/pgpainless-sop/src/test/java/org/pgpainless/sop/ListProfilesTest.java new file mode 100644 index 00000000..0103bf4d --- /dev/null +++ b/pgpainless-sop/src/test/java/org/pgpainless/sop/ListProfilesTest.java @@ -0,0 +1,34 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.sop; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import sop.SOP; +import sop.exception.SOPGPException; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; + +public class ListProfilesTest { + + private SOP sop; + + @BeforeEach + public void prepare() { + this.sop = new SOPImpl(); + } + + @Test + public void listProfilesOfGenerateKey() { + assertFalse(sop.listProfiles().subcommand("generate-key").isEmpty()); + } + + @Test + public void listProfilesOfHelpCommandThrows() { + assertThrows(SOPGPException.UnsupportedProfile.class, () -> + sop.listProfiles().subcommand("help")); + } +} From 702fdf085c2eef616be59b233988d5ab2a28f417 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 17 Apr 2023 12:49:23 +0200 Subject: [PATCH 246/763] Thin out and rename profiles of generate-key --- .../java/org/pgpainless/sop/GenerateKeyImpl.java | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/GenerateKeyImpl.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/GenerateKeyImpl.java index 693ca454..d2baf29b 100644 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/GenerateKeyImpl.java +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/GenerateKeyImpl.java @@ -33,16 +33,15 @@ import sop.operation.GenerateKey; */ public class GenerateKeyImpl implements GenerateKey { - public static final Profile DEFAULT_PROFILE = new Profile("default", "Generate keys based on XDH and EdDSA"); - public static final Profile RSA3072_PROFILE = new Profile("rfc4880-rsa3072@pgpainless.org", "Generate 3072-bit RSA keys"); - public static final Profile RSA4096_PROFILE = new Profile("rfc4880-rsa4096@pgpainless.org", "Generate 4096-bit RSA keys"); + public static final Profile CURVE25519_PROFILE = new Profile("draft-koch-eddsa-for-openpgp-00", "Generate EdDSA / ECDH keys using Curve25519"); + public static final Profile RSA4096_PROFILE = new Profile("rfc4880", "Generate 4096-bit RSA keys"); - public static final List SUPPORTED_PROFILES = Arrays.asList(DEFAULT_PROFILE, RSA3072_PROFILE, RSA4096_PROFILE); + public static final List SUPPORTED_PROFILES = Arrays.asList(CURVE25519_PROFILE, RSA4096_PROFILE); private boolean armor = true; private final Set userIds = new LinkedHashSet<>(); private Passphrase passphrase = Passphrase.emptyPassphrase(); - private String profile = DEFAULT_PROFILE.getName(); + private String profile = CURVE25519_PROFILE.getName(); @Override public GenerateKey noArmor() { @@ -117,14 +116,11 @@ public class GenerateKeyImpl implements GenerateKey { throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException { PGPSecretKeyRing key; // XDH + EdDSA - if (profile.equals(DEFAULT_PROFILE.getName())) { + if (profile.equals(CURVE25519_PROFILE.getName())) { key = PGPainless.generateKeyRing() .modernKeyRing(primaryUserId, passphrase); } - else if (profile.equals(RSA3072_PROFILE.getName())) { - key = PGPainless.generateKeyRing() - .simpleRsaKeyRing(primaryUserId, RsaLength._3072, passphrase); - } + // RSA 4096 else if (profile.equals(RSA4096_PROFILE.getName())) { key = PGPainless.generateKeyRing() .simpleRsaKeyRing(primaryUserId, RsaLength._4096, passphrase); From 63714859292c7bac51b3e40d127439cf469a93ad Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 17 Apr 2023 14:51:50 +0200 Subject: [PATCH 247/763] Add some clarifying comments to GenerateKeyImpl --- .../src/main/java/org/pgpainless/sop/GenerateKeyImpl.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/GenerateKeyImpl.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/GenerateKeyImpl.java index d2baf29b..ba788dac 100644 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/GenerateKeyImpl.java +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/GenerateKeyImpl.java @@ -63,13 +63,16 @@ public class GenerateKeyImpl implements GenerateKey { @Override public GenerateKey profile(String profileName) { + // Sanitize the profile name to make sure we support the given profile for (Profile profile : SUPPORTED_PROFILES) { if (profile.getName().equals(profileName)) { this.profile = profileName; + // return if we found the profile return this; } } + // profile not found, throw throw new SOPGPException.UnsupportedProfile("generate-key", profileName); } @@ -126,6 +129,7 @@ public class GenerateKeyImpl implements GenerateKey { .simpleRsaKeyRing(primaryUserId, RsaLength._4096, passphrase); } else { + // Missing else-if branch for profile. Oops. throw new SOPGPException.UnsupportedProfile("generate-key", profile); } return key; From 11eda9be953487e2124fba4b48c31f72dbb13a94 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 17 Apr 2023 15:09:15 +0200 Subject: [PATCH 248/763] Bump sop-java to 5.0.0 --- version.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.gradle b/version.gradle index 1adf151a..4a532368 100644 --- a/version.gradle +++ b/version.gradle @@ -18,6 +18,6 @@ allprojects { logbackVersion = '1.2.11' mockitoVersion = '4.5.1' slf4jVersion = '1.7.36' - sopJavaVersion = '5.0.0-SNAPSHOT' + sopJavaVersion = '5.0.0' } } From 772a98b4ae0701571136be3dd7ff66f7c17042c0 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 17 Apr 2023 15:22:38 +0200 Subject: [PATCH 249/763] Update changelog --- CHANGELOG.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index bd7f3505..6212d8c4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,14 @@ SPDX-License-Identifier: CC0-1.0 # PGPainless Changelog +## 1.5.0-SNAPSHOT +- Introduce `OpenPgpv6Fingerprint` class +- Bump `sop-java` to `5.0.0`, implementing [SOP Spec Revision 05](https://www.ietf.org/archive/id/draft-dkg-openpgp-stateless-cli-05.html) + - Add support for `list-profiles` subcommand (`generate-key` only for now) + - `generate-key`: Add support for `--profile=` option + - Add profile `draft-koch-eddsa-for-openpgp-00` which represents status quo. + - Add profile `rfc4880` which generates keys based on 4096-bit RSA. + ## 1.4.4 - Fix expectations on subpackets of v3 signatures (thanks @bjansen) - Properly verify v3 signatures, which do not yet have signature subpackets, yet we required them to have From 66d81660052e6a3720eec3026509dcd9d1288491 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Fri, 14 Apr 2023 14:35:11 +0200 Subject: [PATCH 250/763] Bump sop-java to 6.0.0-SNAPSHOT --- version.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.gradle b/version.gradle index 4a532368..aa025a9e 100644 --- a/version.gradle +++ b/version.gradle @@ -18,6 +18,6 @@ allprojects { logbackVersion = '1.2.11' mockitoVersion = '4.5.1' slf4jVersion = '1.7.36' - sopJavaVersion = '5.0.0' + sopJavaVersion = '6.0.0' } } From 446d121777b47f82aa4bfd993d13ef27880e9d77 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Fri, 14 Apr 2023 14:36:31 +0200 Subject: [PATCH 251/763] Bump SOP version in VersionImpl to 06 --- .../src/main/java/org/pgpainless/sop/VersionImpl.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/VersionImpl.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/VersionImpl.java index 13a5dc94..5e851706 100644 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/VersionImpl.java +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/VersionImpl.java @@ -18,7 +18,7 @@ import sop.operation.Version; public class VersionImpl implements Version { // draft version - private static final String SOP_VERSION = "05"; + private static final String SOP_VERSION = "06"; @Override public String getName() { From 5b363de6e42a9cec16175f6afb5552548b389384 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Fri, 14 Apr 2023 14:36:57 +0200 Subject: [PATCH 252/763] Implement VersionImpl.getSopSpecVersion() --- .../src/main/java/org/pgpainless/sop/VersionImpl.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/VersionImpl.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/VersionImpl.java index 5e851706..8986e295 100644 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/VersionImpl.java +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/VersionImpl.java @@ -63,4 +63,9 @@ public class VersionImpl implements Version { "Using " + getBackendVersion() + "\n" + "https://www.bouncycastle.org/java.html"; } + + @Override + public String getSopSpecVersion() { + return "draft-dkg-openpgp-stateless-cli-" + SOP_VERSION; + } } From 3b1edb076c5aa74403777f58f82064d38ef9d2ce Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Fri, 14 Apr 2023 15:13:43 +0200 Subject: [PATCH 253/763] Basic support for sop encrypt --profile=XXX --- .../java/org/pgpainless/sop/EncryptImpl.java | 20 +++++++++++++++++++ .../org/pgpainless/sop/ListProfilesImpl.java | 3 +++ 2 files changed, 23 insertions(+) diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/EncryptImpl.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/EncryptImpl.java index 62d20bf0..689e07be 100644 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/EncryptImpl.java +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/EncryptImpl.java @@ -8,7 +8,9 @@ import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.nio.charset.Charset; +import java.util.Arrays; import java.util.HashSet; +import java.util.List; import java.util.Set; import org.bouncycastle.openpgp.PGPException; @@ -28,6 +30,7 @@ import org.pgpainless.exception.WrongPassphraseException; import org.pgpainless.key.OpenPgpFingerprint; import org.pgpainless.key.info.KeyRingInfo; import org.pgpainless.util.Passphrase; +import sop.Profile; import sop.Ready; import sop.enums.EncryptAs; import sop.exception.SOPGPException; @@ -39,10 +42,15 @@ import sop.util.ProxyOutputStream; */ public class EncryptImpl implements Encrypt { + private static final Profile DEFAULT_PROFILE = new Profile("default", "Use the implementer's recommendations"); + + public static final List SUPPORTED_PROFILES = Arrays.asList(DEFAULT_PROFILE); + EncryptionOptions encryptionOptions = EncryptionOptions.get(); SigningOptions signingOptions = null; MatchMakingSecretKeyRingProtector protector = new MatchMakingSecretKeyRingProtector(); private final Set signingKeys = new HashSet<>(); + private String profile = DEFAULT_PROFILE.getName(); // TODO: Use in future releases private EncryptAs encryptAs = EncryptAs.Binary; boolean armor = true; @@ -111,6 +119,18 @@ public class EncryptImpl implements Encrypt { return this; } + @Override + public Encrypt profile(String profileName) { + for (Profile profile : SUPPORTED_PROFILES) { + if (profile.getName().equals(profileName)) { + this.profile = profile.getName(); + return this; + } + } + + throw new SOPGPException.UnsupportedProfile("encrypt", profileName); + } + @Override public Ready plaintext(InputStream plaintext) throws IOException { if (!encryptionOptions.hasEncryptionMethod()) { diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/ListProfilesImpl.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/ListProfilesImpl.java index 06519bb3..c0f5027a 100644 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/ListProfilesImpl.java +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/ListProfilesImpl.java @@ -22,6 +22,9 @@ public class ListProfilesImpl implements ListProfiles { case "generate-key": return GenerateKeyImpl.SUPPORTED_PROFILES; + case "encrypt": + return EncryptImpl.SUPPORTED_PROFILES; + default: throw new SOPGPException.UnsupportedProfile(command); } From 926e540016dd6edf8ec7de2234847e3fbbbbc4dc Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Fri, 14 Apr 2023 15:57:29 +0200 Subject: [PATCH 254/763] Test fine-grained SOP spec version --- .../cli/commands/VersionCmdTest.java | 8 +++++++ .../java/org/pgpainless/sop/VersionImpl.java | 21 +++++++++++++---- .../java/org/pgpainless/sop/VersionTest.java | 23 +++++++++++++++++++ 3 files changed, 47 insertions(+), 5 deletions(-) diff --git a/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/VersionCmdTest.java b/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/VersionCmdTest.java index 2e4aa7e4..87f535a8 100644 --- a/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/VersionCmdTest.java +++ b/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/VersionCmdTest.java @@ -41,4 +41,12 @@ public class VersionCmdTest extends CLITest { assertTrue(info.contains("Bouncy Castle")); assertTrue(info.contains("Stateless OpenPGP Protocol")); } + + @Test + public void testSopSpecVersion() throws IOException { + ByteArrayOutputStream out = pipeStdoutToStream(); + assertSuccess(executeCommand("version", "--sop-spec")); + String info = out.toString(); + assertTrue(info.startsWith("draft-dkg-openpgp-stateless-cli-")); + } } diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/VersionImpl.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/VersionImpl.java index 8986e295..82995edf 100644 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/VersionImpl.java +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/VersionImpl.java @@ -18,7 +18,7 @@ import sop.operation.Version; public class VersionImpl implements Version { // draft version - private static final String SOP_VERSION = "06"; + private static final int SOP_VERSION = 6; @Override public String getName() { @@ -51,11 +51,12 @@ public class VersionImpl implements Version { @Override public String getExtendedVersion() { + String FORMAT_VERSION = String.format("%02d", SOP_VERSION); return getName() + " " + getVersion() + "\n" + "https://codeberg.org/PGPainless/pgpainless/src/branch/master/pgpainless-sop\n" + "\n" + - "Implementation of the Stateless OpenPGP Protocol Version " + SOP_VERSION + "\n" + - "https://datatracker.ietf.org/doc/html/draft-dkg-openpgp-stateless-cli-" + SOP_VERSION + "\n" + + "Implementation of the Stateless OpenPGP Protocol Version " + FORMAT_VERSION + "\n" + + "https://datatracker.ietf.org/doc/html/draft-dkg-openpgp-stateless-cli-" + FORMAT_VERSION + "\n" + "\n" + "Based on pgpainless-core " + getVersion() + "\n" + "https://pgpainless.org\n" + @@ -65,7 +66,17 @@ public class VersionImpl implements Version { } @Override - public String getSopSpecVersion() { - return "draft-dkg-openpgp-stateless-cli-" + SOP_VERSION; + public int getSopSpecVersionNumber() { + return SOP_VERSION; + } + + @Override + public boolean isSopSpecImplementationIncomplete() { + return false; + } + + @Override + public String getSopSpecImplementationIncompletenessRemarks() { + return null; } } diff --git a/pgpainless-sop/src/test/java/org/pgpainless/sop/VersionTest.java b/pgpainless-sop/src/test/java/org/pgpainless/sop/VersionTest.java index c9739471..fa4af6a8 100644 --- a/pgpainless-sop/src/test/java/org/pgpainless/sop/VersionTest.java +++ b/pgpainless-sop/src/test/java/org/pgpainless/sop/VersionTest.java @@ -7,6 +7,7 @@ package org.pgpainless.sop; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; @@ -49,4 +50,26 @@ public class VersionTest { String firstLine = extendedVersion.split("\n")[0]; assertEquals(sop.version().getName() + " " + sop.version().getVersion(), firstLine); } + + @Test + public void testGetSopSpecVersion() { + boolean incomplete = sop.version().isSopSpecImplementationIncomplete(); + int revisionNumber = sop.version().getSopSpecVersionNumber(); + + String revisionString = sop.version().getSopSpecRevisionString(); + assertEquals("draft-dkg-openpgp-stateless-cli-" + String.format("%02d", revisionNumber), revisionString); + + String incompletenessRemarks = sop.version().getSopSpecImplementationIncompletenessRemarks(); + + String fullSopSpecVersion = sop.version().getSopSpecVersion(); + if (incomplete) { + assertTrue(fullSopSpecVersion.startsWith("~" + revisionString)); + } else { + assertTrue(fullSopSpecVersion.startsWith(revisionString)); + } + + if (incompletenessRemarks != null) { + assertTrue(fullSopSpecVersion.endsWith(incompletenessRemarks)); + } + } } From 676e7d166a22854b53f01a50c6416a8c2f5c1920 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 17 Apr 2023 16:06:45 +0200 Subject: [PATCH 255/763] EncryptImpl: Rename default profile, add documentation --- .../src/main/java/org/pgpainless/sop/EncryptImpl.java | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/EncryptImpl.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/EncryptImpl.java index 689e07be..18bcabcc 100644 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/EncryptImpl.java +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/EncryptImpl.java @@ -42,15 +42,15 @@ import sop.util.ProxyOutputStream; */ public class EncryptImpl implements Encrypt { - private static final Profile DEFAULT_PROFILE = new Profile("default", "Use the implementer's recommendations"); + private static final Profile RFC4880_PROFILE = new Profile("rfc4880", "Follow the packet format of rfc4880"); - public static final List SUPPORTED_PROFILES = Arrays.asList(DEFAULT_PROFILE); + public static final List SUPPORTED_PROFILES = Arrays.asList(RFC4880_PROFILE); EncryptionOptions encryptionOptions = EncryptionOptions.get(); SigningOptions signingOptions = null; MatchMakingSecretKeyRingProtector protector = new MatchMakingSecretKeyRingProtector(); private final Set signingKeys = new HashSet<>(); - private String profile = DEFAULT_PROFILE.getName(); // TODO: Use in future releases + private String profile = RFC4880_PROFILE.getName(); // TODO: Use in future releases private EncryptAs encryptAs = EncryptAs.Binary; boolean armor = true; @@ -121,13 +121,16 @@ public class EncryptImpl implements Encrypt { @Override public Encrypt profile(String profileName) { + // sanitize profile name to make sure we only accept supported profiles for (Profile profile : SUPPORTED_PROFILES) { if (profile.getName().equals(profileName)) { + // profile is supported, return this.profile = profile.getName(); return this; } } + // Profile is not supported, throw throw new SOPGPException.UnsupportedProfile("encrypt", profileName); } From 003423a165631e8015cbe05f424fc0555b3ae0cf Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 17 Apr 2023 16:09:51 +0200 Subject: [PATCH 256/763] Update changelog --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6212d8c4..7d408157 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,10 @@ SPDX-License-Identifier: CC0-1.0 - `generate-key`: Add support for `--profile=` option - Add profile `draft-koch-eddsa-for-openpgp-00` which represents status quo. - Add profile `rfc4880` which generates keys based on 4096-bit RSA. +- Bump `sop-java` to `6.0.0`, implementing [SOP Spec Revision 06](https://www.ietf.org/archive/id/draft-dkg-openpgp-stateless-cli-06.html) + - `encrypt`: Add support for `--profile=` option + - Add profile `rfc4880` to reflect status quo + - `version`: Add support for `--sop-spec` option ## 1.4.4 - Fix expectations on subpackets of v3 signatures (thanks @bjansen) From e465ae60a7150faabfeedb32dee3d727fb01662e Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 17 Apr 2023 16:22:54 +0200 Subject: [PATCH 257/763] VersionImpl: Fix outdated method names --- .../src/main/java/org/pgpainless/sop/VersionImpl.java | 5 +++-- .../src/test/java/org/pgpainless/sop/VersionTest.java | 6 +++--- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/VersionImpl.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/VersionImpl.java index 82995edf..0794c708 100644 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/VersionImpl.java +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/VersionImpl.java @@ -66,7 +66,7 @@ public class VersionImpl implements Version { } @Override - public int getSopSpecVersionNumber() { + public int getSopSpecRevisionNumber() { return SOP_VERSION; } @@ -76,7 +76,8 @@ public class VersionImpl implements Version { } @Override - public String getSopSpecImplementationIncompletenessRemarks() { + public String getSopSpecImplementationRemarks() { return null; } + } diff --git a/pgpainless-sop/src/test/java/org/pgpainless/sop/VersionTest.java b/pgpainless-sop/src/test/java/org/pgpainless/sop/VersionTest.java index fa4af6a8..32d2c2e0 100644 --- a/pgpainless-sop/src/test/java/org/pgpainless/sop/VersionTest.java +++ b/pgpainless-sop/src/test/java/org/pgpainless/sop/VersionTest.java @@ -54,12 +54,12 @@ public class VersionTest { @Test public void testGetSopSpecVersion() { boolean incomplete = sop.version().isSopSpecImplementationIncomplete(); - int revisionNumber = sop.version().getSopSpecVersionNumber(); + int revisionNumber = sop.version().getSopSpecRevisionNumber(); - String revisionString = sop.version().getSopSpecRevisionString(); + String revisionString = sop.version().getSopSpecRevisionName(); assertEquals("draft-dkg-openpgp-stateless-cli-" + String.format("%02d", revisionNumber), revisionString); - String incompletenessRemarks = sop.version().getSopSpecImplementationIncompletenessRemarks(); + String incompletenessRemarks = sop.version().getSopSpecImplementationRemarks(); String fullSopSpecVersion = sop.version().getSopSpecVersion(); if (incomplete) { From 3a3e193bb0466ec4b5618c5fc40645f0e2645a23 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 17 Apr 2023 16:23:16 +0200 Subject: [PATCH 258/763] Bump bouncycastle to 1.73 --- version.gradle | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/version.gradle b/version.gradle index aa025a9e..cbd64528 100644 --- a/version.gradle +++ b/version.gradle @@ -8,12 +8,8 @@ allprojects { isSnapshot = true pgpainlessMinAndroidSdk = 10 javaSourceCompatibility = 1.8 - bouncyCastleVersion = '1.72' - // When using bouncyCastleVersion 1.72: - // unfortunately we rely on 1.72.1 or 1.72.3 for a patch for https://github.com/bcgit/bc-java/issues/1257 - // which is a bug we introduced with a PR against BC :/ oops - // When bouncyCastleVersion is 1.71, bouncyPgVersion can simply be set to 1.71 as well. - bouncyPgVersion = '1.72.3' + bouncyCastleVersion = '1.73' + bouncyPgVersion = bouncyCastleVersion junitVersion = '5.8.2' logbackVersion = '1.2.11' mockitoVersion = '4.5.1' From d8f32b668910b7c1a53ae288bcffbdb013464b07 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 17 Apr 2023 16:24:55 +0200 Subject: [PATCH 259/763] Update changelog --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7d408157..08184f70 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,8 @@ SPDX-License-Identifier: CC0-1.0 # PGPainless Changelog ## 1.5.0-SNAPSHOT +- Bump `bcpg-jdk15to18` to `1.73` +- Bump `bcprov-jdk15to18` to `1.73` - Introduce `OpenPgpv6Fingerprint` class - Bump `sop-java` to `5.0.0`, implementing [SOP Spec Revision 05](https://www.ietf.org/archive/id/draft-dkg-openpgp-stateless-cli-05.html) - Add support for `list-profiles` subcommand (`generate-key` only for now) From 94a609127e75f1569e42e17f68a32029c039389f Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 17 Apr 2023 16:35:17 +0200 Subject: [PATCH 260/763] PGPainless 1.5.0 --- CHANGELOG.md | 2 +- README.md | 2 +- pgpainless-sop/README.md | 6 +++--- version.gradle | 4 ++-- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 08184f70..51f7a815 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ SPDX-License-Identifier: CC0-1.0 # PGPainless Changelog -## 1.5.0-SNAPSHOT +## 1.5.0 - Bump `bcpg-jdk15to18` to `1.73` - Bump `bcprov-jdk15to18` to `1.73` - Introduce `OpenPgpv6Fingerprint` class diff --git a/README.md b/README.md index 2a91789e..14ff388d 100644 --- a/README.md +++ b/README.md @@ -191,7 +191,7 @@ repositories { } dependencies { - implementation 'org.pgpainless:pgpainless-core:1.4.4' + implementation 'org.pgpainless:pgpainless-core:1.5.0' } ``` diff --git a/pgpainless-sop/README.md b/pgpainless-sop/README.md index 3aef6d05..b4e755dd 100644 --- a/pgpainless-sop/README.md +++ b/pgpainless-sop/README.md @@ -6,7 +6,7 @@ SPDX-License-Identifier: Apache-2.0 # PGPainless-SOP -[![Spec Revision: 4](https://img.shields.io/badge/Spec%20Revision-4-blue)](https://datatracker.ietf.org/doc/draft-dkg-openpgp-stateless-cli/) +[![Spec Revision: 6](https://img.shields.io/badge/Spec%20Revision-6-blue)](https://datatracker.ietf.org/doc/draft-dkg-openpgp-stateless-cli/) [![Maven Central](https://badgen.net/maven/v/maven-central/org.pgpainless/pgpainless-sop)](https://search.maven.org/artifact/org.pgpainless/pgpainless-sop) [![javadoc](https://javadoc.io/badge2/org.pgpainless/pgpainless-sop/javadoc.svg)](https://javadoc.io/doc/org.pgpainless/pgpainless-sop) @@ -23,7 +23,7 @@ To start using pgpainless-sop in your code, include the following lines in your ... dependencies { ... - implementation "org.pgpainless:pgpainless-sop:1.4.4" + implementation "org.pgpainless:pgpainless-sop:1.5.0" ... } @@ -34,7 +34,7 @@ dependencies { org.pgpainless pgpainless-sop - 1.4.4 + 1.5.0 ... diff --git a/version.gradle b/version.gradle index cbd64528..86239e88 100644 --- a/version.gradle +++ b/version.gradle @@ -4,8 +4,8 @@ allprojects { ext { - shortVersion = '1.4.5' - isSnapshot = true + shortVersion = '1.5.0' + isSnapshot = false pgpainlessMinAndroidSdk = 10 javaSourceCompatibility = 1.8 bouncyCastleVersion = '1.73' From 36a52a3e34c76287b0a37ee22c70b8ec57657180 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 17 Apr 2023 16:37:37 +0200 Subject: [PATCH 261/763] PGPainless 1.5.1-SNAPSHOT --- version.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/version.gradle b/version.gradle index 86239e88..3d271a75 100644 --- a/version.gradle +++ b/version.gradle @@ -4,8 +4,8 @@ allprojects { ext { - shortVersion = '1.5.0' - isSnapshot = false + shortVersion = '1.5.1' + isSnapshot = true pgpainlessMinAndroidSdk = 10 javaSourceCompatibility = 1.8 bouncyCastleVersion = '1.73' From 9a0b60ac7e45e2f6cae2c6d88e334507348e17d1 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 18 Apr 2023 17:41:02 +0200 Subject: [PATCH 262/763] Update quickstart document --- docs/source/pgpainless-sop/quickstart.md | 40 +++++++++++++++++++++++- 1 file changed, 39 insertions(+), 1 deletion(-) diff --git a/docs/source/pgpainless-sop/quickstart.md b/docs/source/pgpainless-sop/quickstart.md index 10ef0a72..60e1df29 100644 --- a/docs/source/pgpainless-sop/quickstart.md +++ b/docs/source/pgpainless-sop/quickstart.md @@ -75,10 +75,21 @@ In both cases, the resulting output will be the UTF8 encoded, ASCII armored Open To disable ASCII armoring, call `noArmor()` before calling `generate()`. -At the time of writing, the resulting OpenPGP secret key will consist of a certification-capable 256-bits +Revision `05` of the Stateless OpenPGP Protocol specification introduced the concept of profiles for +certain operations. +The key generation feature is the first operation to make use of profiles to specify different key algorithms. +To set a profile, simply call `profile(String profileName)` and pass in one of the available profile identifiers. + +To explore, which profiles are available, refer to the dedicated [section](#explore-profiles). + +The default profile used by `pgpainless-sop` is called `draft-koch-eddsa-for-openpgp-00`. +If this profile is used, the resulting OpenPGP secret key will consist of a certification-capable 256-bits ed25519 EdDSA primary key, a 256-bits ed25519 EdDSA subkey used for signing, as well as a 256-bits X25519 ECDH subkey for encryption. +Another profile defined by `pgpainless-sop` is `rfc4880`, which changes the key generation behaviour such that +the resulting key is a single 4096-bit RSA key capable of certifying, signing and encrypting. + The whole key does not have an expiration date set. ### Extract a Certificate @@ -186,6 +197,13 @@ If any keys used for signing are password protected, you need to provide the sig It does not matter in which order signing keys and key passwords are provided, the implementation will figure out matches on its own. If different key passwords are used, the `withKeyPassword(_)` method can be called multiple times. +You can modify the behaviour of the encrypt operation by switching between different profiles via the +`profile(String profileName)` method. +At the time of writing, the only available profile for this operation is `rfc4880` which applies encryption +as defined in [rfc4880](https://datatracker.ietf.org/doc/html/rfc4880). + +To explore, which profiles are available, refer to the dedicated [section](#explore-profiles). + By default, the encrypted message will be ASCII armored. To disable ASCII armor, call `noArmor()` before the `plaintext(_)` method call. @@ -464,3 +482,23 @@ By default, the signatures output will be ASCII armored. This can be disabled by prior to `message(_)`. The detached signatures can now be verified like in the section above. + +### Explore Profiles + +Certain operations allow modification of their behaviour by selecting between different profiles. +An example for this is the `generateKey()` operation, where different profiles result in different algorithms used +during key generation. + +To explore, which profiles are supported by a certain operation, you can use the `listProfiles()` operation. +For example, this is how you can get a list of profiles supported by the `generateKey()` operation: + +```java +List profiles = sop.listProfiles().subcommand("generate-key"); +``` + +:::{note} +As you can see, the argument passed into the `subcommand()` method must match the operation name as defined in the +[Stateless OpenPGP Protocol specification](https://datatracker.ietf.org/doc/draft-dkg-openpgp-stateless-cli/). +::: + +At the time of writing (the latest revision of the SOP spec is 06), only `generate-key` and `encrypt` accept profiles. \ No newline at end of file From 05968533a5b64005a38702b7e2fdf787c59265ba Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 18 Apr 2023 18:35:47 +0200 Subject: [PATCH 263/763] InlineVerifyImpl: Export signature mode in Verification result --- .../RoundTripInlineSignInlineVerifyCmdTest.java | 2 +- .../org/pgpainless/sop/InlineVerifyImpl.java | 17 ++++++++++++++++- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/RoundTripInlineSignInlineVerifyCmdTest.java b/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/RoundTripInlineSignInlineVerifyCmdTest.java index d36ee58f..0676f213 100644 --- a/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/RoundTripInlineSignInlineVerifyCmdTest.java +++ b/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/RoundTripInlineSignInlineVerifyCmdTest.java @@ -409,7 +409,7 @@ public class RoundTripInlineSignInlineVerifyCmdTest extends CLITest { assertEquals("Hello, World!\n", out.toString()); String ver = readStringFromFile(verifications); assertEquals( - "2022-11-18T14:55:33Z 7A073EDF273C902796D259528FBDD36D01831673 AEA0FD2C899D3FC077815F0026560D2AE53DB86F\n", ver); + "2022-11-18T14:55:33Z 7A073EDF273C902796D259528FBDD36D01831673 AEA0FD2C899D3FC077815F0026560D2AE53DB86F mode:binary\n", ver); } @Test diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/InlineVerifyImpl.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/InlineVerifyImpl.java index 7665a7bb..eea5ebfb 100644 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/InlineVerifyImpl.java +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/InlineVerifyImpl.java @@ -13,6 +13,7 @@ import java.util.List; import org.bouncycastle.openpgp.PGPException; import org.bouncycastle.openpgp.PGPPublicKeyRingCollection; +import org.bouncycastle.openpgp.PGPSignature; import org.bouncycastle.util.io.Streams; import org.pgpainless.PGPainless; import org.pgpainless.decryption_verification.ConsumerOptions; @@ -23,6 +24,7 @@ import org.pgpainless.exception.MalformedOpenPgpMessageException; import org.pgpainless.exception.MissingDecryptionMethodException; import sop.ReadyWithResult; import sop.Verification; +import sop.enums.SignatureMode; import sop.exception.SOPGPException; import sop.operation.InlineVerify; @@ -96,6 +98,19 @@ public class InlineVerifyImpl implements InlineVerify { private Verification map(SignatureVerification sigVerification) { return new Verification(sigVerification.getSignature().getCreationTime(), sigVerification.getSigningKey().getSubkeyFingerprint().toString(), - sigVerification.getSigningKey().getPrimaryKeyFingerprint().toString()); + sigVerification.getSigningKey().getPrimaryKeyFingerprint().toString(), + getMode(sigVerification.getSignature()), + null); + } + + private static SignatureMode getMode(PGPSignature signature) { + if (signature.getSignatureType() == PGPSignature.BINARY_DOCUMENT) { + return SignatureMode.binary; + } + if (signature.getSignatureType() == PGPSignature.CANONICAL_TEXT_DOCUMENT) { + return SignatureMode.text; + } + + return null; } } From 2ec176e9388e281f98171b89008e84d0f0cb125c Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 18 Apr 2023 18:39:52 +0200 Subject: [PATCH 264/763] DetachedVerifyImpl: Export signature mode in Verification result --- .../cli/commands/InlineDetachCmdTest.java | 4 ++-- .../commands/RoundTripSignVerifyCmdTest.java | 4 ++-- .../org/pgpainless/sop/DetachedVerifyImpl.java | 17 ++++++++++++++++- 3 files changed, 20 insertions(+), 5 deletions(-) diff --git a/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/InlineDetachCmdTest.java b/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/InlineDetachCmdTest.java index 8854d837..19bc9aa5 100644 --- a/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/InlineDetachCmdTest.java +++ b/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/InlineDetachCmdTest.java @@ -90,7 +90,7 @@ public class InlineDetachCmdTest extends CLITest { pipeStringToStdin(msgOut.toString()); ByteArrayOutputStream verifyOut = pipeStdoutToStream(); assertSuccess(executeCommand("verify", sigFile.getAbsolutePath(), certFile.getAbsolutePath())); - assertEquals("2021-05-15T16:08:06Z 4F665C4DC2C4660BC6425E415736E6931ACF370C 4F665C4DC2C4660BC6425E415736E6931ACF370C\n", + assertEquals("2021-05-15T16:08:06Z 4F665C4DC2C4660BC6425E415736E6931ACF370C 4F665C4DC2C4660BC6425E415736E6931ACF370C mode:text\n", verifyOut.toString()); } @@ -115,7 +115,7 @@ public class InlineDetachCmdTest extends CLITest { ByteArrayOutputStream verifyOut = pipeStdoutToStream(); File certFile = writeFile("cert.asc", CERT); assertSuccess(executeCommand("verify", sigFile.getAbsolutePath(), certFile.getAbsolutePath())); - assertEquals("2021-05-15T16:08:06Z 4F665C4DC2C4660BC6425E415736E6931ACF370C 4F665C4DC2C4660BC6425E415736E6931ACF370C\n", + assertEquals("2021-05-15T16:08:06Z 4F665C4DC2C4660BC6425E415736E6931ACF370C 4F665C4DC2C4660BC6425E415736E6931ACF370C mode:text\n", verifyOut.toString()); } diff --git a/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/RoundTripSignVerifyCmdTest.java b/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/RoundTripSignVerifyCmdTest.java index 6196a847..97bfae7e 100644 --- a/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/RoundTripSignVerifyCmdTest.java +++ b/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/RoundTripSignVerifyCmdTest.java @@ -94,7 +94,7 @@ public class RoundTripSignVerifyCmdTest extends CLITest { "=VWAZ\n" + "-----END PGP SIGNATURE-----"; private static final String BINARY_SIG_VERIFICATION = - "2022-11-09T18:40:24Z 444C10AB011EF8424C83F0A9DA9F413986211DC6 9DA09423C9F94BA4CCA30951099B11BF296A373E\n"; + "2022-11-09T18:40:24Z 444C10AB011EF8424C83F0A9DA9F413986211DC6 9DA09423C9F94BA4CCA30951099B11BF296A373E mode:binary\n"; private static final String TEXT_SIG = "-----BEGIN PGP SIGNATURE-----\n" + "Version: PGPainless\n" + "\n" + @@ -104,7 +104,7 @@ public class RoundTripSignVerifyCmdTest extends CLITest { "=s5xn\n" + "-----END PGP SIGNATURE-----"; private static final String TEXT_SIG_VERIFICATION = - "2022-11-09T18:41:18Z 444C10AB011EF8424C83F0A9DA9F413986211DC6 9DA09423C9F94BA4CCA30951099B11BF296A373E\n"; + "2022-11-09T18:41:18Z 444C10AB011EF8424C83F0A9DA9F413986211DC6 9DA09423C9F94BA4CCA30951099B11BF296A373E mode:text\n"; private static final Date TEXT_SIG_CREATION = UTCUtil.parseUTCDate("2022-11-09T18:41:18Z"); @Test diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/DetachedVerifyImpl.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/DetachedVerifyImpl.java index 93ad398c..f0cb1161 100644 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/DetachedVerifyImpl.java +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/DetachedVerifyImpl.java @@ -12,6 +12,7 @@ import java.util.List; import org.bouncycastle.openpgp.PGPException; import org.bouncycastle.openpgp.PGPPublicKeyRingCollection; +import org.bouncycastle.openpgp.PGPSignature; import org.bouncycastle.util.io.Streams; import org.pgpainless.PGPainless; import org.pgpainless.decryption_verification.ConsumerOptions; @@ -20,6 +21,7 @@ import org.pgpainless.decryption_verification.MessageMetadata; import org.pgpainless.decryption_verification.SignatureVerification; import org.pgpainless.exception.MalformedOpenPgpMessageException; import sop.Verification; +import sop.enums.SignatureMode; import sop.exception.SOPGPException; import sop.operation.DetachedVerify; @@ -94,6 +96,19 @@ public class DetachedVerifyImpl implements DetachedVerify { private Verification map(SignatureVerification sigVerification) { return new Verification(sigVerification.getSignature().getCreationTime(), sigVerification.getSigningKey().getSubkeyFingerprint().toString(), - sigVerification.getSigningKey().getPrimaryKeyFingerprint().toString()); + sigVerification.getSigningKey().getPrimaryKeyFingerprint().toString(), + getMode(sigVerification.getSignature()), + null); + } + + private static SignatureMode getMode(PGPSignature signature) { + if (signature.getSignatureType() == PGPSignature.BINARY_DOCUMENT) { + return SignatureMode.binary; + } + if (signature.getSignatureType() == PGPSignature.CANONICAL_TEXT_DOCUMENT) { + return SignatureMode.text; + } + + return null; } } From b0974c6ade4569c9f7342884d45d1a80a212e5c4 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 18 Apr 2023 18:40:25 +0200 Subject: [PATCH 265/763] Add more tests for inline-sign-verify roundtrips --- .../sop/InlineSignVerifyRoundtripTest.java | 50 +++++++++++++++++-- 1 file changed, 47 insertions(+), 3 deletions(-) diff --git a/pgpainless-sop/src/test/java/org/pgpainless/sop/InlineSignVerifyRoundtripTest.java b/pgpainless-sop/src/test/java/org/pgpainless/sop/InlineSignVerifyRoundtripTest.java index b24b729c..e5ce518b 100644 --- a/pgpainless-sop/src/test/java/org/pgpainless/sop/InlineSignVerifyRoundtripTest.java +++ b/pgpainless-sop/src/test/java/org/pgpainless/sop/InlineSignVerifyRoundtripTest.java @@ -9,13 +9,14 @@ import sop.ByteArrayAndResult; import sop.SOP; import sop.Verification; import sop.enums.InlineSignAs; +import sop.enums.SignatureMode; import java.io.IOException; import java.nio.charset.StandardCharsets; import java.util.List; import static org.junit.jupiter.api.Assertions.assertArrayEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertEquals; public class InlineSignVerifyRoundtripTest { @@ -46,7 +47,11 @@ public class InlineSignVerifyRoundtripTest { byte[] verified = result.getBytes(); - assertFalse(result.getResult().isEmpty()); + List verificationList = result.getResult(); + assertEquals(1, verificationList.size()); + Verification verification = verificationList.get(0); + assertEquals(SignatureMode.text, verification.getSignatureMode()); + assertArrayEquals(message, verified); } @@ -65,6 +70,7 @@ public class InlineSignVerifyRoundtripTest { byte[] inlineSigned = sop.inlineSign() .key(key) .withKeyPassword("sw0rdf1sh") + .mode(InlineSignAs.binary) .data(message).getBytes(); ByteArrayAndResult> result = sop.inlineVerify() @@ -74,7 +80,45 @@ public class InlineSignVerifyRoundtripTest { byte[] verified = result.getBytes(); - assertFalse(result.getResult().isEmpty()); + List verificationList = result.getResult(); + assertEquals(1, verificationList.size()); + Verification verification = verificationList.get(0); + assertEquals(SignatureMode.binary, verification.getSignatureMode()); + + assertArrayEquals(message, verified); + } + + + @Test + public void testInlineSignAndVerifyWithTextSignatures() throws IOException { + byte[] key = sop.generateKey() + .userId("Mark") + .withKeyPassword("y3110w5ubm4r1n3") + .generate().getBytes(); + + byte[] cert = sop.extractCert() + .key(key).getBytes(); + + byte[] message = "Give me a plaintext that I can sign and verify, pls.".getBytes(StandardCharsets.UTF_8); + + byte[] inlineSigned = sop.inlineSign() + .key(key) + .withKeyPassword("y3110w5ubm4r1n3") + .mode(InlineSignAs.text) + .data(message).getBytes(); + + ByteArrayAndResult> result = sop.inlineVerify() + .cert(cert) + .data(inlineSigned) + .toByteArrayAndResult(); + + byte[] verified = result.getBytes(); + + List verificationList = result.getResult(); + assertEquals(1, verificationList.size()); + Verification verification = verificationList.get(0); + assertEquals(SignatureMode.text, verification.getSignatureMode()); + assertArrayEquals(message, verified); } From 06c924d41dff1a358ea07eb9ddefe4b1984c6185 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 18 Apr 2023 18:43:56 +0200 Subject: [PATCH 266/763] Add tests for mode to DetachedSignTest --- .../org/pgpainless/sop/DetachedSignTest.java | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/pgpainless-sop/src/test/java/org/pgpainless/sop/DetachedSignTest.java b/pgpainless-sop/src/test/java/org/pgpainless/sop/DetachedSignTest.java index c6fcc267..4f274691 100644 --- a/pgpainless-sop/src/test/java/org/pgpainless/sop/DetachedSignTest.java +++ b/pgpainless-sop/src/test/java/org/pgpainless/sop/DetachedSignTest.java @@ -23,6 +23,7 @@ import org.pgpainless.signature.SignatureUtils; import sop.SOP; import sop.Verification; import sop.enums.SignAs; +import sop.enums.SignatureMode; import sop.exception.SOPGPException; public class DetachedSignTest { @@ -49,6 +50,7 @@ public class DetachedSignTest { public void signArmored() throws IOException { byte[] signature = sop.sign() .key(key) + .mode(SignAs.Binary) .data(data) .toByteArrayAndResult().getBytes(); @@ -62,6 +64,7 @@ public class DetachedSignTest { .data(data); assertEquals(1, verifications.size()); + assertEquals(SignatureMode.binary, verifications.get(0).getSignatureMode()); } @Test @@ -84,6 +87,26 @@ public class DetachedSignTest { assertEquals(1, verifications.size()); } + @Test + public void textSig() throws IOException { + byte[] signature = sop.sign() + .key(key) + .noArmor() + .mode(SignAs.Text) + .data(data) + .toByteArrayAndResult().getBytes(); + + List verifications = sop.verify() + .cert(cert) + .notAfter(new Date(new Date().getTime() + 10000)) + .notBefore(new Date(new Date().getTime() - 10000)) + .signatures(signature) + .data(data); + + assertEquals(1, verifications.size()); + assertEquals(SignatureMode.text, verifications.get(0).getSignatureMode()); + } + @Test public void rejectSignatureAsTooOld() throws IOException { byte[] signature = sop.sign() From e3bacdbe35cef9aa7bf0d36d08fe7399747d5478 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 18 Apr 2023 18:53:50 +0200 Subject: [PATCH 267/763] Introduce VerificationHelper class and export signature mode in decrypt operation --- .../RoundTripEncryptDecryptCmdTest.java | 4 +- .../java/org/pgpainless/sop/DecryptImpl.java | 8 +-- .../pgpainless/sop/DetachedVerifyImpl.java | 23 +------- .../org/pgpainless/sop/InlineVerifyImpl.java | 23 +------- .../pgpainless/sop/VerificationHelper.java | 52 +++++++++++++++++++ 5 files changed, 57 insertions(+), 53 deletions(-) create mode 100644 pgpainless-sop/src/main/java/org/pgpainless/sop/VerificationHelper.java diff --git a/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/RoundTripEncryptDecryptCmdTest.java b/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/RoundTripEncryptDecryptCmdTest.java index d314de20..8c294e59 100644 --- a/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/RoundTripEncryptDecryptCmdTest.java +++ b/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/RoundTripEncryptDecryptCmdTest.java @@ -129,7 +129,7 @@ public class RoundTripEncryptDecryptCmdTest extends CLITest { String romeosVerif = readStringFromFile(anotherVerificationsFile); assertEquals(julietsVerif, romeosVerif); assertFalse(julietsVerif.isEmpty()); - assertEquals(103, julietsVerif.length()); // 103 is number of symbols in [DATE, FINGER, FINGER] for V4 + assertEquals(115, julietsVerif.length()); // 115 is number of symbols in [DATE, FINGER, FINGER, MODE] for V4 } @Test @@ -274,7 +274,7 @@ public class RoundTripEncryptDecryptCmdTest extends CLITest { assertEquals(plaintext, out.toString()); String verificationString = readStringFromFile(verifications); - assertEquals("2022-11-09T17:22:48Z C0DCEC44B1A173664B05DABCECD0BF863F65C9A5 A2EC077FC977E15DD799EFF92C0D3C123CF51C08\n", + assertEquals("2022-11-09T17:22:48Z C0DCEC44B1A173664B05DABCECD0BF863F65C9A5 A2EC077FC977E15DD799EFF92C0D3C123CF51C08 mode:binary\n", verificationString); } diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/DecryptImpl.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/DecryptImpl.java index f7876799..d15713ca 100644 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/DecryptImpl.java +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/DecryptImpl.java @@ -147,7 +147,7 @@ public class DecryptImpl implements Decrypt { List verificationList = new ArrayList<>(); for (SignatureVerification signatureVerification : metadata.getVerifiedInlineSignatures()) { - verificationList.add(map(signatureVerification)); + verificationList.add(VerificationHelper.mapVerification(signatureVerification)); } SessionKey sessionKey = null; @@ -163,10 +163,4 @@ public class DecryptImpl implements Decrypt { } }; } - - private Verification map(SignatureVerification sigVerification) { - return new Verification(sigVerification.getSignature().getCreationTime(), - sigVerification.getSigningKey().getSubkeyFingerprint().toString(), - sigVerification.getSigningKey().getPrimaryKeyFingerprint().toString()); - } } diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/DetachedVerifyImpl.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/DetachedVerifyImpl.java index f0cb1161..cdae0215 100644 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/DetachedVerifyImpl.java +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/DetachedVerifyImpl.java @@ -12,7 +12,6 @@ import java.util.List; import org.bouncycastle.openpgp.PGPException; import org.bouncycastle.openpgp.PGPPublicKeyRingCollection; -import org.bouncycastle.openpgp.PGPSignature; import org.bouncycastle.util.io.Streams; import org.pgpainless.PGPainless; import org.pgpainless.decryption_verification.ConsumerOptions; @@ -21,7 +20,6 @@ import org.pgpainless.decryption_verification.MessageMetadata; import org.pgpainless.decryption_verification.SignatureVerification; import org.pgpainless.exception.MalformedOpenPgpMessageException; import sop.Verification; -import sop.enums.SignatureMode; import sop.exception.SOPGPException; import sop.operation.DetachedVerify; @@ -78,7 +76,7 @@ public class DetachedVerifyImpl implements DetachedVerify { List verificationList = new ArrayList<>(); for (SignatureVerification signatureVerification : metadata.getVerifiedDetachedSignatures()) { - verificationList.add(map(signatureVerification)); + verificationList.add(VerificationHelper.mapVerification(signatureVerification)); } if (!options.getCertificateSource().getExplicitCertificates().isEmpty()) { @@ -92,23 +90,4 @@ public class DetachedVerifyImpl implements DetachedVerify { throw new SOPGPException.BadData(e); } } - - private Verification map(SignatureVerification sigVerification) { - return new Verification(sigVerification.getSignature().getCreationTime(), - sigVerification.getSigningKey().getSubkeyFingerprint().toString(), - sigVerification.getSigningKey().getPrimaryKeyFingerprint().toString(), - getMode(sigVerification.getSignature()), - null); - } - - private static SignatureMode getMode(PGPSignature signature) { - if (signature.getSignatureType() == PGPSignature.BINARY_DOCUMENT) { - return SignatureMode.binary; - } - if (signature.getSignatureType() == PGPSignature.CANONICAL_TEXT_DOCUMENT) { - return SignatureMode.text; - } - - return null; - } } diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/InlineVerifyImpl.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/InlineVerifyImpl.java index eea5ebfb..aecb891b 100644 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/InlineVerifyImpl.java +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/InlineVerifyImpl.java @@ -13,7 +13,6 @@ import java.util.List; import org.bouncycastle.openpgp.PGPException; import org.bouncycastle.openpgp.PGPPublicKeyRingCollection; -import org.bouncycastle.openpgp.PGPSignature; import org.bouncycastle.util.io.Streams; import org.pgpainless.PGPainless; import org.pgpainless.decryption_verification.ConsumerOptions; @@ -24,7 +23,6 @@ import org.pgpainless.exception.MalformedOpenPgpMessageException; import org.pgpainless.exception.MissingDecryptionMethodException; import sop.ReadyWithResult; import sop.Verification; -import sop.enums.SignatureMode; import sop.exception.SOPGPException; import sop.operation.InlineVerify; @@ -76,7 +74,7 @@ public class InlineVerifyImpl implements InlineVerify { metadata.getVerifiedInlineSignatures(); for (SignatureVerification signatureVerification : verifications) { - verificationList.add(map(signatureVerification)); + verificationList.add(VerificationHelper.mapVerification(signatureVerification)); } if (!options.getCertificateSource().getExplicitCertificates().isEmpty()) { @@ -94,23 +92,4 @@ public class InlineVerifyImpl implements InlineVerify { } }; } - - private Verification map(SignatureVerification sigVerification) { - return new Verification(sigVerification.getSignature().getCreationTime(), - sigVerification.getSigningKey().getSubkeyFingerprint().toString(), - sigVerification.getSigningKey().getPrimaryKeyFingerprint().toString(), - getMode(sigVerification.getSignature()), - null); - } - - private static SignatureMode getMode(PGPSignature signature) { - if (signature.getSignatureType() == PGPSignature.BINARY_DOCUMENT) { - return SignatureMode.binary; - } - if (signature.getSignatureType() == PGPSignature.CANONICAL_TEXT_DOCUMENT) { - return SignatureMode.text; - } - - return null; - } } diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/VerificationHelper.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/VerificationHelper.java new file mode 100644 index 00000000..126a5e3b --- /dev/null +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/VerificationHelper.java @@ -0,0 +1,52 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.sop; + +import org.bouncycastle.openpgp.PGPSignature; +import org.pgpainless.decryption_verification.SignatureVerification; +import sop.Verification; +import sop.enums.SignatureMode; + +/** + * Helper class for shared methods related to {@link Verification Verifications}. + */ +public class VerificationHelper { + + /** + * Map a {@link SignatureVerification} object to a {@link Verification}. + * + * @param sigVerification signature verification + * @return verification + */ + public static Verification mapVerification(SignatureVerification sigVerification) { + return new Verification( + sigVerification.getSignature().getCreationTime(), + sigVerification.getSigningKey().getSubkeyFingerprint().toString(), + sigVerification.getSigningKey().getPrimaryKeyFingerprint().toString(), + getMode(sigVerification.getSignature()), + null); + } + + /** + * Map an OpenPGP signature type to a {@link SignatureMode} enum. + * Note: This method only maps {@link PGPSignature#BINARY_DOCUMENT} and {@link PGPSignature#CANONICAL_TEXT_DOCUMENT}. + * Other values are mapped to
      null
      . + * + * @param signature signature + * @return signature mode enum or null + */ + private static SignatureMode getMode(PGPSignature signature) { + + if (signature.getSignatureType() == PGPSignature.BINARY_DOCUMENT) { + return SignatureMode.binary; + } + + if (signature.getSignatureType() == PGPSignature.CANONICAL_TEXT_DOCUMENT) { + return SignatureMode.text; + } + + return null; + } +} From d5f3dc80bca5706c7381e8163f42a0584bde6fb7 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 18 Apr 2023 19:00:33 +0200 Subject: [PATCH 268/763] Update changelog --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 51f7a815..893638c3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,9 @@ SPDX-License-Identifier: CC0-1.0 # PGPainless Changelog +## 1.5.1-SNAPSHOT +- SOP: Emit signature `mode:{binary|text}` in `Verification` results + ## 1.5.0 - Bump `bcpg-jdk15to18` to `1.73` - Bump `bcprov-jdk15to18` to `1.73` From d10841c57a0a76a2092ef7f7ab5d9436d2fcf51c Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 24 Apr 2023 16:13:11 +0200 Subject: [PATCH 269/763] Add workflow for pull requests --- .github/workflows/pr.yml | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 .github/workflows/pr.yml diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml new file mode 100644 index 00000000..cc1ca4e0 --- /dev/null +++ b/.github/workflows/pr.yml @@ -0,0 +1,33 @@ +# SPDX-FileCopyrightText: 2021 Paul Schaub +# +# SPDX-License-Identifier: Apache-2.0 + +# This workflow uses actions that are not certified by GitHub. +# They are provided by a third-party and are governed by +# separate terms of service, privacy policy, and support +# documentation. +# This workflow will build a Java project with Gradle and cache/restore any dependencies to improve the workflow execution time +# For more information see: https://help.github.com/actions/language-and-framework-guides/building-and-testing-java-with-gradle + +name: Build + +on: + pull_request: + +permissions: + contents: read + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - name: Set up JDK 11 + uses: actions/setup-java@v3 + with: + java-version: '11' + distribution: 'temurin' + - name: Build, Check and Coverage + uses: gradle/gradle-build-action@67421db6bd0bf253fb4bd25b31ebb98943c375e1 + with: + arguments: check jacocoRootReport From 0cb088525193ce845ee4cb51d66ac7c21513ddc1 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 25 Apr 2023 13:28:07 +0200 Subject: [PATCH 270/763] Relax constraints on decryption keys to improve interop with faulty, broken legacy clients that have been very naughty and need punishment --- .../OpenPgpMessageInputStream.java | 9 ++-- .../org/pgpainless/key/info/KeyRingInfo.java | 46 +++++++++++++++++++ ...ntDecryptionUsingNonEncryptionKeyTest.java | 18 ++++++++ 3 files changed, 68 insertions(+), 5 deletions(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java index 7fe11bbf..19a01fbf 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java @@ -43,15 +43,14 @@ import org.bouncycastle.openpgp.operator.SessionKeyDataDecryptorFactory; import org.bouncycastle.util.io.TeeInputStream; import org.pgpainless.PGPainless; import org.pgpainless.algorithm.CompressionAlgorithm; -import org.pgpainless.algorithm.EncryptionPurpose; import org.pgpainless.algorithm.OpenPgpPacket; import org.pgpainless.algorithm.StreamEncoding; import org.pgpainless.algorithm.SymmetricKeyAlgorithm; +import org.pgpainless.decryption_verification.cleartext_signatures.ClearsignedMessageUtil; +import org.pgpainless.decryption_verification.cleartext_signatures.MultiPassStrategy; import org.pgpainless.decryption_verification.syntax_check.InputSymbol; import org.pgpainless.decryption_verification.syntax_check.PDA; import org.pgpainless.decryption_verification.syntax_check.StackSymbol; -import org.pgpainless.decryption_verification.cleartext_signatures.ClearsignedMessageUtil; -import org.pgpainless.decryption_verification.cleartext_signatures.MultiPassStrategy; import org.pgpainless.exception.MalformedOpenPgpMessageException; import org.pgpainless.exception.MessageNotIntegrityProtectedException; import org.pgpainless.exception.MissingDecryptionMethodException; @@ -674,7 +673,7 @@ public class OpenPgpMessageInputStream extends DecryptionStream { for (PGPSecretKeyRing secretKeys : options.getDecryptionKeys()) { KeyRingInfo info = PGPainless.inspectKeyRing(secretKeys); - for (PGPPublicKey publicKey : info.getEncryptionSubkeys(EncryptionPurpose.ANY)) { + for (PGPPublicKey publicKey : info.getDecryptionSubkeys()) { if (publicKey.getAlgorithm() == algorithm && info.isSecretKeyAvailable(publicKey.getKeyID())) { PGPSecretKey candidate = secretKeys.getSecretKey(publicKey.getKeyID()); decryptionKeyCandidates.add(new Tuple<>(secretKeys, candidate)); @@ -692,7 +691,7 @@ public class OpenPgpMessageInputStream extends DecryptionStream { } KeyRingInfo info = new KeyRingInfo(secretKeys, policy, new Date()); - List encryptionKeys = info.getEncryptionSubkeys(EncryptionPurpose.ANY); + List encryptionKeys = info.getDecryptionSubkeys(); for (PGPPublicKey key : encryptionKeys) { if (key.getKeyID() == keyID) { return secretKeys; diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/info/KeyRingInfo.java b/pgpainless-core/src/main/java/org/pgpainless/key/info/KeyRingInfo.java index 1ebd023e..75d9e16c 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/key/info/KeyRingInfo.java +++ b/pgpainless-core/src/main/java/org/pgpainless/key/info/KeyRingInfo.java @@ -41,11 +41,15 @@ import org.pgpainless.algorithm.SymmetricKeyAlgorithm; import org.pgpainless.exception.KeyException; import org.pgpainless.key.OpenPgpFingerprint; import org.pgpainless.key.SubkeyIdentifier; +import org.pgpainless.key.util.KeyIdUtil; import org.pgpainless.key.util.RevocationAttributes; import org.pgpainless.policy.Policy; import org.pgpainless.signature.SignatureUtils; import org.pgpainless.signature.consumer.SignaturePicker; import org.pgpainless.signature.subpackets.SignatureSubpacketsUtil; +import org.pgpainless.util.DateUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; /** * Utility class to quickly extract certain information from a {@link PGPPublicKeyRing}/{@link PGPSecretKeyRing}. @@ -55,6 +59,8 @@ public class KeyRingInfo { private static final Pattern PATTERN_EMAIL_FROM_USERID = Pattern.compile("<([a-zA-Z0-9_!#$%&'*+/=?`{|}~^.-]+@[a-zA-Z0-9.-]+)>"); private static final Pattern PATTERN_EMAIL_EXPLICIT = Pattern.compile("^([a-zA-Z0-9_!#$%&'*+/=?`{|}~^.-]+@[a-zA-Z0-9.-]+)$"); + private static final Logger LOGGER = LoggerFactory.getLogger(KeyRingInfo.class); + private final PGPKeyRing keys; private final Signatures signatures; private final Date referenceDate; @@ -904,6 +910,7 @@ public class KeyRingInfo { public @Nonnull List getEncryptionSubkeys(EncryptionPurpose purpose) { Date primaryExpiration = getPrimaryKeyExpirationDate(); if (primaryExpiration != null && primaryExpiration.before(referenceDate)) { + LOGGER.debug("Certificate is expired: Primary key is expired on " + DateUtil.formatUTCDate(primaryExpiration)); return Collections.emptyList(); } @@ -913,15 +920,18 @@ public class KeyRingInfo { PGPPublicKey subKey = subkeys.next(); if (!isKeyValidlyBound(subKey.getKeyID())) { + LOGGER.debug("(Sub?)-Key " + KeyIdUtil.formatKeyId(subKey.getKeyID()) + " is not validly bound."); continue; } Date subkeyExpiration = getSubkeyExpirationDate(OpenPgpFingerprint.of(subKey)); if (subkeyExpiration != null && subkeyExpiration.before(referenceDate)) { + LOGGER.debug("(Sub?)-Key " + KeyIdUtil.formatKeyId(subKey.getKeyID()) + " is expired on " + DateUtil.formatUTCDate(subkeyExpiration)); continue; } if (!subKey.isEncryptionKey()) { + LOGGER.debug("(Sub?)-Key " + KeyIdUtil.formatKeyId(subKey.getKeyID()) + " algorithm is not capable of encryption."); continue; } @@ -947,6 +957,42 @@ public class KeyRingInfo { return encryptionKeys; } + /** + * Return a list of all subkeys that could potentially be used to decrypt a message. + * Contrary to {@link #getEncryptionSubkeys(EncryptionPurpose)}, this method also includes revoked, expired keys, + * as well as keys which do not carry any encryption keyflags. + * Merely keys which use algorithms that cannot be used for encryption at all are excluded. + * That way, decryption of messages produced by faulty implementations can still be decrypted. + * + * @return decryption keys + */ + public @Nonnull List getDecryptionSubkeys() { + Iterator subkeys = keys.getPublicKeys(); + List decryptionKeys = new ArrayList<>(); + + while (subkeys.hasNext()) { + PGPPublicKey subKey = subkeys.next(); + + // subkeys have been valid at some point + if (subKey.getKeyID() != getKeyId()) { + PGPSignature binding = signatures.subkeyBindings.get(subKey.getKeyID()); + if (binding == null) { + LOGGER.debug("Subkey " + KeyIdUtil.formatKeyId(subKey.getKeyID()) + " was never validly bound."); + continue; + } + } + + // Public-Key algorithm can encrypt + if (!subKey.isEncryptionKey()) { + LOGGER.debug("(Sub?)-Key " + KeyIdUtil.formatKeyId(subKey.getKeyID()) + " is not encryption-capable."); + continue; + } + + decryptionKeys.add(subKey); + } + return decryptionKeys; + } + /** * Return a list of all keys which carry the provided key flag in their signature. * diff --git a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/PreventDecryptionUsingNonEncryptionKeyTest.java b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/PreventDecryptionUsingNonEncryptionKeyTest.java index ba80c69d..04a98265 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/PreventDecryptionUsingNonEncryptionKeyTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/PreventDecryptionUsingNonEncryptionKeyTest.java @@ -14,6 +14,7 @@ import java.nio.charset.StandardCharsets; import org.bouncycastle.openpgp.PGPException; import org.bouncycastle.openpgp.PGPSecretKeyRing; import org.bouncycastle.util.io.Streams; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.pgpainless.PGPainless; import org.pgpainless.exception.MissingDecryptionMethodException; @@ -189,6 +190,23 @@ public class PreventDecryptionUsingNonEncryptionKeyTest { } @Test + public void canDecryptMessageDespiteMissingKeyFlag() throws IOException, PGPException { + PGPSecretKeyRing secretKeys = PGPainless.readKeyRing().secretKeyRing(ENCRYPTION_INCAPABLE_KEY); + + ByteArrayInputStream msgIn = new ByteArrayInputStream(MSG.getBytes(StandardCharsets.UTF_8)); + DecryptionStream decryptionStream = PGPainless.decryptAndOrVerify() + .onInputStream(msgIn) + .withOptions(new ConsumerOptions().addDecryptionKey(secretKeys)); + + Streams.drain(decryptionStream); + decryptionStream.close(); + OpenPgpMetadata metadata = decryptionStream.getResult(); + + assertEquals(new SubkeyIdentifier(secretKeys, secretKeys.getPublicKey().getKeyID()), metadata.getDecryptionKey()); + } + + @Test + @Disabled public void nonEncryptionKeyCannotDecrypt() throws IOException { PGPSecretKeyRing secretKeys = PGPainless.readKeyRing().secretKeyRing(ENCRYPTION_INCAPABLE_KEY); From 699381238c97089b4c9e3e27cb9c091ed687c654 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 25 Apr 2023 13:28:38 +0200 Subject: [PATCH 271/763] Update changelog --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 893638c3..22e452e6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ SPDX-License-Identifier: CC0-1.0 ## 1.5.1-SNAPSHOT - SOP: Emit signature `mode:{binary|text}` in `Verification` results +- core: Relax constraints on decryption subkeys to improve interoperability with broken clients + - Allow decryption with revoked keys + - Allow decryption with expired keys + - Allow decryption with erroneously addressed keys without encryption key flags ## 1.5.0 - Bump `bcpg-jdk15to18` to `1.73` From bf2bb31b711d14ca872f6d6d6f49034f98540797 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 25 Apr 2023 13:36:48 +0200 Subject: [PATCH 272/763] PGPainless 1.5.1 --- CHANGELOG.md | 2 +- README.md | 2 +- pgpainless-sop/README.md | 4 ++-- version.gradle | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 22e452e6..d59bf372 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ SPDX-License-Identifier: CC0-1.0 # PGPainless Changelog -## 1.5.1-SNAPSHOT +## 1.5.1 - SOP: Emit signature `mode:{binary|text}` in `Verification` results - core: Relax constraints on decryption subkeys to improve interoperability with broken clients - Allow decryption with revoked keys diff --git a/README.md b/README.md index 14ff388d..06e9f2ed 100644 --- a/README.md +++ b/README.md @@ -191,7 +191,7 @@ repositories { } dependencies { - implementation 'org.pgpainless:pgpainless-core:1.5.0' + implementation 'org.pgpainless:pgpainless-core:1.5.1' } ``` diff --git a/pgpainless-sop/README.md b/pgpainless-sop/README.md index b4e755dd..7ae0a63c 100644 --- a/pgpainless-sop/README.md +++ b/pgpainless-sop/README.md @@ -23,7 +23,7 @@ To start using pgpainless-sop in your code, include the following lines in your ... dependencies { ... - implementation "org.pgpainless:pgpainless-sop:1.5.0" + implementation "org.pgpainless:pgpainless-sop:1.5.1" ... } @@ -34,7 +34,7 @@ dependencies { org.pgpainless pgpainless-sop - 1.5.0 + 1.5.1 ... diff --git a/version.gradle b/version.gradle index 3d271a75..88c3babd 100644 --- a/version.gradle +++ b/version.gradle @@ -5,7 +5,7 @@ allprojects { ext { shortVersion = '1.5.1' - isSnapshot = true + isSnapshot = false pgpainlessMinAndroidSdk = 10 javaSourceCompatibility = 1.8 bouncyCastleVersion = '1.73' From 23fd630670e1d734d22effad5843a5e9ada49b49 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 25 Apr 2023 13:39:44 +0200 Subject: [PATCH 273/763] PGPainless 1.5.2-SNAPSHOT --- version.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/version.gradle b/version.gradle index 88c3babd..c53ce511 100644 --- a/version.gradle +++ b/version.gradle @@ -4,8 +4,8 @@ allprojects { ext { - shortVersion = '1.5.1' - isSnapshot = false + shortVersion = '1.5.2' + isSnapshot = true pgpainlessMinAndroidSdk = 10 javaSourceCompatibility = 1.8 bouncyCastleVersion = '1.73' From eb1ff27a900e56d97872fd0b167dbb2b07f3ac2c Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 27 Apr 2023 15:15:42 +0200 Subject: [PATCH 274/763] Bump sop-java to 6.1.0 --- .../commands/RoundTripSignVerifyCmdTest.java | 11 ++++++- .../sop/CarolKeySignEncryptRoundtripTest.java | 10 +++--- .../org/pgpainless/sop/DetachedSignTest.java | 14 +++++--- .../sop/EncryptDecryptRoundTripTest.java | 31 ++++++++++++------ .../org/pgpainless/sop/InlineDetachTest.java | 24 ++++++++------ .../sop/InlineSignVerifyRoundtripTest.java | 32 +++++++++---------- version.gradle | 2 +- 7 files changed, 79 insertions(+), 45 deletions(-) diff --git a/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/RoundTripSignVerifyCmdTest.java b/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/RoundTripSignVerifyCmdTest.java index 97bfae7e..0ff83144 100644 --- a/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/RoundTripSignVerifyCmdTest.java +++ b/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/RoundTripSignVerifyCmdTest.java @@ -13,6 +13,7 @@ import java.io.File; import java.io.IOException; import java.security.InvalidAlgorithmParameterException; import java.security.NoSuchAlgorithmException; +import java.text.ParseException; import java.util.Date; import org.bouncycastle.openpgp.PGPException; @@ -105,7 +106,15 @@ public class RoundTripSignVerifyCmdTest extends CLITest { "-----END PGP SIGNATURE-----"; private static final String TEXT_SIG_VERIFICATION = "2022-11-09T18:41:18Z 444C10AB011EF8424C83F0A9DA9F413986211DC6 9DA09423C9F94BA4CCA30951099B11BF296A373E mode:text\n"; - private static final Date TEXT_SIG_CREATION = UTCUtil.parseUTCDate("2022-11-09T18:41:18Z"); + private static final Date TEXT_SIG_CREATION; + + static { + try { + TEXT_SIG_CREATION = UTCUtil.parseUTCDate("2022-11-09T18:41:18Z"); + } catch (ParseException e) { + throw new RuntimeException(e); + } + } @Test public void createArmoredSignature() throws IOException { diff --git a/pgpainless-sop/src/test/java/org/pgpainless/sop/CarolKeySignEncryptRoundtripTest.java b/pgpainless-sop/src/test/java/org/pgpainless/sop/CarolKeySignEncryptRoundtripTest.java index 81bf7645..58dfaa62 100644 --- a/pgpainless-sop/src/test/java/org/pgpainless/sop/CarolKeySignEncryptRoundtripTest.java +++ b/pgpainless-sop/src/test/java/org/pgpainless/sop/CarolKeySignEncryptRoundtripTest.java @@ -4,15 +4,15 @@ package org.pgpainless.sop; +import static org.junit.jupiter.api.Assertions.assertArrayEquals; + import java.io.IOException; import org.junit.jupiter.api.Test; import sop.ByteArrayAndResult; import sop.DecryptionResult; import sop.Ready; - -import static org.junit.jupiter.api.Assertions.assertArrayEquals; -import static org.junit.jupiter.api.Assertions.assertEquals; +import sop.testsuite.assertions.VerificationListAssert; public class CarolKeySignEncryptRoundtripTest { @@ -290,6 +290,8 @@ public class CarolKeySignEncryptRoundtripTest { byte[] plaintext = decryption.getBytes(); assertArrayEquals(msg, plaintext); - assertEquals(1, decryption.getResult().getVerifications().size()); + VerificationListAssert.assertThatVerificationList(decryption.getResult().getVerifications()) + .hasSingleItem() + .issuedBy("71FFDA004409E5DDB0C3E8F19BA789DC76D6849A", "71FFDA004409E5DDB0C3E8F19BA789DC76D6849A"); } } diff --git a/pgpainless-sop/src/test/java/org/pgpainless/sop/DetachedSignTest.java b/pgpainless-sop/src/test/java/org/pgpainless/sop/DetachedSignTest.java index 4f274691..316c4a37 100644 --- a/pgpainless-sop/src/test/java/org/pgpainless/sop/DetachedSignTest.java +++ b/pgpainless-sop/src/test/java/org/pgpainless/sop/DetachedSignTest.java @@ -25,6 +25,7 @@ import sop.Verification; import sop.enums.SignAs; import sop.enums.SignatureMode; import sop.exception.SOPGPException; +import sop.testsuite.assertions.VerificationListAssert; public class DetachedSignTest { @@ -63,8 +64,9 @@ public class DetachedSignTest { .signatures(signature) .data(data); - assertEquals(1, verifications.size()); - assertEquals(SignatureMode.binary, verifications.get(0).getSignatureMode()); + VerificationListAssert.assertThatVerificationList(verifications) + .hasSingleItem() + .hasMode(SignatureMode.binary); } @Test @@ -84,7 +86,8 @@ public class DetachedSignTest { .signatures(signature) .data(data); - assertEquals(1, verifications.size()); + VerificationListAssert.assertThatVerificationList(verifications) + .hasSingleItem(); } @Test @@ -103,8 +106,9 @@ public class DetachedSignTest { .signatures(signature) .data(data); - assertEquals(1, verifications.size()); - assertEquals(SignatureMode.text, verifications.get(0).getSignatureMode()); + VerificationListAssert.assertThatVerificationList(verifications) + .hasSingleItem() + .hasMode(SignatureMode.text); } @Test diff --git a/pgpainless-sop/src/test/java/org/pgpainless/sop/EncryptDecryptRoundTripTest.java b/pgpainless-sop/src/test/java/org/pgpainless/sop/EncryptDecryptRoundTripTest.java index 22af5b70..f51b711d 100644 --- a/pgpainless-sop/src/test/java/org/pgpainless/sop/EncryptDecryptRoundTripTest.java +++ b/pgpainless-sop/src/test/java/org/pgpainless/sop/EncryptDecryptRoundTripTest.java @@ -21,7 +21,9 @@ import sop.ByteArrayAndResult; import sop.DecryptionResult; import sop.SOP; import sop.SessionKey; +import sop.enums.SignatureMode; import sop.exception.SOPGPException; +import sop.testsuite.assertions.VerificationListAssert; public class EncryptDecryptRoundTripTest { @@ -75,7 +77,8 @@ public class EncryptDecryptRoundTripTest { assertArrayEquals(message, decrypted.toByteArray()); DecryptionResult result = bytesAndResult.getResult(); - assertEquals(1, result.getVerifications().size()); + VerificationListAssert.assertThatVerificationList(result.getVerifications()) + .hasSingleItem(); } @Test @@ -106,7 +109,8 @@ public class EncryptDecryptRoundTripTest { assertArrayEquals(message, decrypted); DecryptionResult result = bytesAndResult.getResult(); - assertEquals(1, result.getVerifications().size()); + VerificationListAssert.assertThatVerificationList(result.getVerifications()) + .hasSingleItem(); } @Test @@ -125,7 +129,8 @@ public class EncryptDecryptRoundTripTest { assertArrayEquals(message, decrypted); DecryptionResult result = bytesAndResult.getResult(); - assertEquals(0, result.getVerifications().size()); + VerificationListAssert.assertThatVerificationList(result.getVerifications()) + .isEmpty(); } @Test @@ -144,7 +149,8 @@ public class EncryptDecryptRoundTripTest { assertArrayEquals(message, decrypted); DecryptionResult result = bytesAndResult.getResult(); - assertEquals(0, result.getVerifications().size()); + VerificationListAssert.assertThatVerificationList(result.getVerifications()) + .isEmpty(); } @Test @@ -163,7 +169,8 @@ public class EncryptDecryptRoundTripTest { assertArrayEquals(message, decrypted); DecryptionResult result = bytesAndResult.getResult(); - assertEquals(0, result.getVerifications().size()); + VerificationListAssert.assertThatVerificationList(result.getVerifications()) + .isEmpty(); } @Test @@ -180,7 +187,8 @@ public class EncryptDecryptRoundTripTest { .toByteArrayAndResult() .getResult(); - assertTrue(result.getVerifications().isEmpty()); + VerificationListAssert.assertThatVerificationList(result.getVerifications()) + .isEmpty(); } @Test @@ -486,14 +494,19 @@ public class EncryptDecryptRoundTripTest { sop.decrypt().withKey(key).verifyWithCert(cert).ciphertext(ciphertext).toByteArrayAndResult(); assertEquals(sessionKey, bytesAndResult.getResult().getSessionKey().get().toString()); assertArrayEquals(plaintext, bytesAndResult.getBytes()); - assertEquals(1, bytesAndResult.getResult().getVerifications().size()); - + VerificationListAssert.assertThatVerificationList(bytesAndResult.getResult().getVerifications()) + .hasSingleItem() + .issuedBy("9C26EFAB1C6500A228E8A9C2658EE420C824D191") + .hasMode(SignatureMode.binary); // Decrypt with session key bytesAndResult = sop.decrypt().withSessionKey(SessionKey.fromString(sessionKey)) .verifyWithCert(cert).ciphertext(ciphertext).toByteArrayAndResult(); assertEquals(sessionKey, bytesAndResult.getResult().getSessionKey().get().toString()); assertArrayEquals(plaintext, bytesAndResult.getBytes()); - assertEquals(1, bytesAndResult.getResult().getVerifications().size()); + VerificationListAssert.assertThatVerificationList(bytesAndResult.getResult().getVerifications()) + .hasSingleItem() + .issuedBy("9C26EFAB1C6500A228E8A9C2658EE420C824D191") + .hasMode(SignatureMode.binary); } @Test diff --git a/pgpainless-sop/src/test/java/org/pgpainless/sop/InlineDetachTest.java b/pgpainless-sop/src/test/java/org/pgpainless/sop/InlineDetachTest.java index 1054babb..98279e4f 100644 --- a/pgpainless-sop/src/test/java/org/pgpainless/sop/InlineDetachTest.java +++ b/pgpainless-sop/src/test/java/org/pgpainless/sop/InlineDetachTest.java @@ -5,8 +5,6 @@ package org.pgpainless.sop; import static org.junit.jupiter.api.Assertions.assertArrayEquals; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; @@ -34,7 +32,9 @@ import sop.SOP; import sop.Signatures; import sop.Verification; import sop.enums.InlineSignAs; +import sop.enums.SignatureMode; import sop.exception.SOPGPException; +import sop.testsuite.assertions.VerificationListAssert; public class InlineDetachTest { @@ -79,9 +79,11 @@ public class InlineDetachTest { .signatures(signature) .data(message); - assertFalse(verificationList.isEmpty()); - assertEquals(1, verificationList.size()); - assertEquals(new OpenPgpV4Fingerprint(secretKey).toString(), verificationList.get(0).getSigningCertFingerprint()); + VerificationListAssert.assertThatVerificationList(verificationList) + .hasSingleItem() + .issuedBy(new OpenPgpV4Fingerprint(secretKey).toString()) + .hasMode(SignatureMode.text); + assertArrayEquals(data, message); } @@ -121,8 +123,10 @@ public class InlineDetachTest { .signatures(signature) .data(message); - assertFalse(verificationList.isEmpty()); - assertEquals(1, verificationList.size()); + VerificationListAssert.assertThatVerificationList(verificationList) + .hasSingleItem() + .hasMode(SignatureMode.binary); + assertArrayEquals(data, message); } @@ -191,8 +195,10 @@ public class InlineDetachTest { .signatures(signature) .data(message); - assertFalse(verificationList.isEmpty()); - assertEquals(1, verificationList.size()); + VerificationListAssert.assertThatVerificationList(verificationList) + .hasSingleItem() + .hasMode(SignatureMode.binary); + assertArrayEquals(data, message); } } diff --git a/pgpainless-sop/src/test/java/org/pgpainless/sop/InlineSignVerifyRoundtripTest.java b/pgpainless-sop/src/test/java/org/pgpainless/sop/InlineSignVerifyRoundtripTest.java index e5ce518b..f3a50fc3 100644 --- a/pgpainless-sop/src/test/java/org/pgpainless/sop/InlineSignVerifyRoundtripTest.java +++ b/pgpainless-sop/src/test/java/org/pgpainless/sop/InlineSignVerifyRoundtripTest.java @@ -4,19 +4,19 @@ package org.pgpainless.sop; +import static org.junit.jupiter.api.Assertions.assertArrayEquals; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.List; + import org.junit.jupiter.api.Test; import sop.ByteArrayAndResult; import sop.SOP; import sop.Verification; import sop.enums.InlineSignAs; import sop.enums.SignatureMode; - -import java.io.IOException; -import java.nio.charset.StandardCharsets; -import java.util.List; - -import static org.junit.jupiter.api.Assertions.assertArrayEquals; -import static org.junit.jupiter.api.Assertions.assertEquals; +import sop.testsuite.assertions.VerificationListAssert; public class InlineSignVerifyRoundtripTest { @@ -48,9 +48,9 @@ public class InlineSignVerifyRoundtripTest { byte[] verified = result.getBytes(); List verificationList = result.getResult(); - assertEquals(1, verificationList.size()); - Verification verification = verificationList.get(0); - assertEquals(SignatureMode.text, verification.getSignatureMode()); + VerificationListAssert.assertThatVerificationList(verificationList) + .hasSingleItem() + .hasMode(SignatureMode.text); assertArrayEquals(message, verified); } @@ -81,9 +81,9 @@ public class InlineSignVerifyRoundtripTest { byte[] verified = result.getBytes(); List verificationList = result.getResult(); - assertEquals(1, verificationList.size()); - Verification verification = verificationList.get(0); - assertEquals(SignatureMode.binary, verification.getSignatureMode()); + VerificationListAssert.assertThatVerificationList(verificationList) + .hasSingleItem() + .hasMode(SignatureMode.binary); assertArrayEquals(message, verified); } @@ -115,9 +115,9 @@ public class InlineSignVerifyRoundtripTest { byte[] verified = result.getBytes(); List verificationList = result.getResult(); - assertEquals(1, verificationList.size()); - Verification verification = verificationList.get(0); - assertEquals(SignatureMode.text, verification.getSignatureMode()); + VerificationListAssert.assertThatVerificationList(verificationList) + .hasSingleItem() + .hasMode(SignatureMode.text); assertArrayEquals(message, verified); } diff --git a/version.gradle b/version.gradle index c53ce511..ce7eebc7 100644 --- a/version.gradle +++ b/version.gradle @@ -14,6 +14,6 @@ allprojects { logbackVersion = '1.2.11' mockitoVersion = '4.5.1' slf4jVersion = '1.7.36' - sopJavaVersion = '6.0.0' + sopJavaVersion = '6.1.0' } } From f51685c1266a17ae55bdd209ce1515c042e9f8c6 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 27 Apr 2023 15:16:37 +0200 Subject: [PATCH 275/763] Update changelog --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d59bf372..506ded3a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,9 @@ SPDX-License-Identifier: CC0-1.0 # PGPainless Changelog +## 1.5.2-SNAPSHOT +- Bump `sop-java` to `6.1.0` + ## 1.5.1 - SOP: Emit signature `mode:{binary|text}` in `Verification` results - core: Relax constraints on decryption subkeys to improve interoperability with broken clients From eb45dee04fdeb9634e78cde1c766b2bef8bec9e9 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 27 Apr 2023 15:22:43 +0200 Subject: [PATCH 276/763] rewriteManPages script: Remind to run Deprecated Gradle features were used in this build, making it incompatible with Gradle 8.0. You can use '--warning-mode all' to show the individual deprecation warnings and determine if they come from your own scripts or plugins. See https://docs.gradle.org/7.5.1/userguide/command_line_interface.html#sec:command_line_warnings in sop repo --- pgpainless-cli/rewriteManPages.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pgpainless-cli/rewriteManPages.sh b/pgpainless-cli/rewriteManPages.sh index 321dbdde..51d2aa04 100755 --- a/pgpainless-cli/rewriteManPages.sh +++ b/pgpainless-cli/rewriteManPages.sh @@ -4,7 +4,7 @@ SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) SOP_DIR=$(realpath $SCRIPT_DIR/../../sop-java) [ ! -d "$SOP_DIR" ] && echo "sop-java repository MUST be cloned next to pgpainless repo" && exit 1; SRC_DIR=$SOP_DIR/sop-java-picocli/build/docs/manpage -[ ! -d "$SRC_DIR" ] && echo "No sop manpages found." && exit 1; +[ ! -d "$SRC_DIR" ] && echo "No sop manpages found. Please run `gradle asciidoctor` in the sop-java repo." && exit 1; DEST_DIR=$SCRIPT_DIR/packaging/man mkdir -p $DEST_DIR From 558036c485d6afbade4380ba5aaddf68ada25906 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 27 Apr 2023 15:23:37 +0200 Subject: [PATCH 277/763] Update man pages --- .../packaging/man/pgpainless-cli-decrypt.1 | 36 ++++----------- .../packaging/man/pgpainless-cli-encrypt.1 | 12 +++-- .../man/pgpainless-cli-generate-completion.1 | 10 ++++ .../man/pgpainless-cli-generate-key.1 | 9 +++- .../packaging/man/pgpainless-cli-help.1 | 10 ++++ .../man/pgpainless-cli-inline-sign.1 | 9 ++-- .../man/pgpainless-cli-list-profiles.1 | 46 +++++++++++++++++++ .../packaging/man/pgpainless-cli-sign.1 | 4 +- .../packaging/man/pgpainless-cli-verify.1 | 13 +++++- .../packaging/man/pgpainless-cli-version.1 | 7 ++- pgpainless-cli/packaging/man/pgpainless-cli.1 | 15 ++++++ 11 files changed, 129 insertions(+), 42 deletions(-) create mode 100644 pgpainless-cli/packaging/man/pgpainless-cli-list-profiles.1 diff --git a/pgpainless-cli/packaging/man/pgpainless-cli-decrypt.1 b/pgpainless-cli/packaging/man/pgpainless-cli-decrypt.1 index 17d59134..258078aa 100644 --- a/pgpainless-cli/packaging/man/pgpainless-cli-decrypt.1 +++ b/pgpainless-cli/packaging/man/pgpainless-cli-decrypt.1 @@ -30,41 +30,21 @@ pgpainless\-cli\-decrypt \- Decrypt a message from standard input .SH "SYNOPSIS" .sp -\fBpgpainless\-cli decrypt\fP [\fB\-\-stacktrace\fP] [\fB\-\-not\-after\fP=\fIDATE\fP] [\fB\-\-not\-before\fP=\fIDATE\fP] -[\fB\-\-session\-key\-out\fP=\fISESSIONKEY\fP] [\fB\-\-verify\-out\fP=\fIVERIFICATIONS\fP] -[\fB\-\-verify\-with\fP=\fICERT\fP]... [\fB\-\-with\-key\-password\fP=\fIPASSWORD\fP]... -[\fB\-\-with\-password\fP=\fIPASSWORD\fP]... [\fB\-\-with\-session\-key\fP=\fISESSIONKEY\fP]... -[\fIKEY\fP...] +\fBpgpainless\-cli decrypt\fP [\fB\-\-stacktrace\fP] [\fB\-\-session\-key\-out\fP=\fISESSIONKEY\fP] +[\fB\-\-verify\-not\-after\fP=\fIDATE\fP] [\fB\-\-verify\-not\-before\fP=\fIDATE\fP] +[\fB\-\-verify\-out\fP=\fIVERIFICATIONS\fP] [\fB\-\-verify\-with\fP=\fICERT\fP]... +[\fB\-\-with\-key\-password\fP=\fIPASSWORD\fP]... [\fB\-\-with\-password\fP=\fIPASSWORD\fP]... +[\fB\-\-with\-session\-key\fP=\fISESSIONKEY\fP]... [\fIKEY\fP...] .SH "DESCRIPTION" .SH "OPTIONS" .sp -\fB\-\-not\-after\fP=\fIDATE\fP -.RS 4 -ISO\-8601 formatted UTC date (e.g. \(aq2020\-11\-23T16:35Z) -.sp -Reject signatures with a creation date not in range. -.sp -Defaults to current system time (\(aqnow\(aq). -.sp -Accepts special value \(aq\-\(aq for end of time. -.RE -.sp -\fB\-\-not\-before\fP=\fIDATE\fP -.RS 4 -ISO\-8601 formatted UTC date (e.g. \(aq2020\-11\-23T16:35Z) -.sp -Reject signatures with a creation date not in range. -.sp -Defaults to beginning of time (\(aq\-\(aq). -.RE -.sp \fB\-\-session\-key\-out\fP=\fISESSIONKEY\fP .RS 4 Can be used to learn the session key on successful decryption .RE .sp -\fB\-\-stacktrace\fP, \fB\-\-verify\-out, \-\-verifications\-out\fP=\fIVERIFICATIONS\fP +\fB\-\-stacktrace\fP, \fB\-\-verify\-not\-after\fP=\fIDATE\fP, \fB\-\-verify\-not\-before\fP=\fIDATE\fP, \fB\-\-verify\-out, \-\-verifications\-out\fP=\fIVERIFICATIONS\fP .RS 4 Emits signature verification status to the designated output .RE @@ -87,7 +67,7 @@ Symmetric passphrase to decrypt the message with. .sp Enables decryption based on any "SKESK" packets in the "CIPHERTEXT". .sp -Is an INDIRECT data type (e.g. file, environment variable, file descriptor...) +Is an INDIRECT data type (e.g. file, environment variable, file descriptor...). .RE .sp \fB\-\-with\-session\-key\fP=\fISESSIONKEY\fP @@ -96,7 +76,7 @@ Symmetric message key (session key). .sp Enables decryption of the "CIPHERTEXT" using the session key directly against the "SEIPD" packet. .sp -Is an INDIRECT data type (e.g. file, environment variable, file descriptor...) +Is an INDIRECT data type (e.g. file, environment variable, file descriptor...). .RE .SH "ARGUMENTS" .sp diff --git a/pgpainless-cli/packaging/man/pgpainless-cli-encrypt.1 b/pgpainless-cli/packaging/man/pgpainless-cli-encrypt.1 index f1d804d0..79002302 100644 --- a/pgpainless-cli/packaging/man/pgpainless-cli-encrypt.1 +++ b/pgpainless-cli/packaging/man/pgpainless-cli-encrypt.1 @@ -31,8 +31,9 @@ pgpainless\-cli\-encrypt \- Encrypt a message from standard input .SH "SYNOPSIS" .sp \fBpgpainless\-cli encrypt\fP [\fB\-\-[no\-]armor\fP] [\fB\-\-stacktrace\fP] [\fB\-\-as\fP=\fI{binary|text}\fP] -[\fB\-\-sign\-with\fP=\fIKEY\fP]... [\fB\-\-with\-key\-password\fP=\fIPASSWORD\fP]... -[\fB\-\-with\-password\fP=\fIPASSWORD\fP]... [\fICERTS\fP...] +[\fB\-\-profile\fP=\fIPROFILE\fP] [\fB\-\-sign\-with\fP=\fIKEY\fP]... +[\fB\-\-with\-key\-password\fP=\fIPASSWORD\fP]... [\fB\-\-with\-password\fP=\fIPASSWORD\fP]... +[\fICERTS\fP...] .SH "DESCRIPTION" .SH "OPTIONS" @@ -47,6 +48,11 @@ Type of the input data. Defaults to \(aqbinary\(aq ASCII armor the output .RE .sp +\fB\-\-profile\fP=\fIPROFILE\fP +.RS 4 +Profile identifier to switch between profiles +.RE +.sp \fB\-\-sign\-with\fP=\fIKEY\fP .RS 4 Sign the output with a private key @@ -63,7 +69,7 @@ Is an INDIRECT data type (e.g. file, environment variable, file descriptor...). .RS 4 Encrypt the message with a password. .sp -Is an INDIRECT data type (e.g. file, environment variable, file descriptor...) +Is an INDIRECT data type (e.g. file, environment variable, file descriptor...). .RE .SH "ARGUMENTS" .sp diff --git a/pgpainless-cli/packaging/man/pgpainless-cli-generate-completion.1 b/pgpainless-cli/packaging/man/pgpainless-cli-generate-completion.1 index 5ab3d673..5c50ee96 100644 --- a/pgpainless-cli/packaging/man/pgpainless-cli-generate-completion.1 +++ b/pgpainless-cli/packaging/man/pgpainless-cli-generate-completion.1 @@ -152,4 +152,14 @@ Ambiguous input (a filename matching the designator already exists) \fB79\fP .RS 4 Key is not signing capable +.RE +.sp +\fB83\fP +.RS 4 +Options were supplied that are incompatible with each other +.RE +.sp +\fB89\fP +.RS 4 +The requested profile is unsupported, or the indicated subcommand does not accept profiles .RE \ No newline at end of file diff --git a/pgpainless-cli/packaging/man/pgpainless-cli-generate-key.1 b/pgpainless-cli/packaging/man/pgpainless-cli-generate-key.1 index 96b069f3..a5317665 100644 --- a/pgpainless-cli/packaging/man/pgpainless-cli-generate-key.1 +++ b/pgpainless-cli/packaging/man/pgpainless-cli-generate-key.1 @@ -30,8 +30,8 @@ pgpainless\-cli\-generate\-key \- Generate a secret key .SH "SYNOPSIS" .sp -\fBpgpainless\-cli generate\-key\fP [\fB\-\-[no\-]armor\fP] [\fB\-\-stacktrace\fP] [\fB\-\-with\-key\-password\fP=\fIPASSWORD\fP] -[\fIUSERID\fP...] +\fBpgpainless\-cli generate\-key\fP [\fB\-\-[no\-]armor\fP] [\fB\-\-stacktrace\fP] [\fB\-\-profile\fP=\fIPROFILE\fP] +[\fB\-\-with\-key\-password\fP=\fIPASSWORD\fP] [\fIUSERID\fP...] .SH "DESCRIPTION" .SH "OPTIONS" @@ -41,6 +41,11 @@ pgpainless\-cli\-generate\-key \- Generate a secret key ASCII armor the output .RE .sp +\fB\-\-profile\fP=\fIPROFILE\fP +.RS 4 +Profile identifier to switch between profiles +.RE +.sp \fB\-\-stacktrace\fP, \fB\-\-with\-key\-password\fP=\fIPASSWORD\fP .RS 4 Password to protect the private key with diff --git a/pgpainless-cli/packaging/man/pgpainless-cli-help.1 b/pgpainless-cli/packaging/man/pgpainless-cli-help.1 index 6152fc87..1e7c2b08 100644 --- a/pgpainless-cli/packaging/man/pgpainless-cli-help.1 +++ b/pgpainless-cli/packaging/man/pgpainless-cli-help.1 @@ -147,4 +147,14 @@ Ambiguous input (a filename matching the designator already exists) \fB79\fP .RS 4 Key is not signing capable +.RE +.sp +\fB83\fP +.RS 4 +Options were supplied that are incompatible with each other +.RE +.sp +\fB89\fP +.RS 4 +The requested profile is unsupported, or the indicated subcommand does not accept profiles .RE \ No newline at end of file diff --git a/pgpainless-cli/packaging/man/pgpainless-cli-inline-sign.1 b/pgpainless-cli/packaging/man/pgpainless-cli-inline-sign.1 index 7deb568c..db041c0c 100644 --- a/pgpainless-cli/packaging/man/pgpainless-cli-inline-sign.1 +++ b/pgpainless-cli/packaging/man/pgpainless-cli-inline-sign.1 @@ -30,20 +30,19 @@ pgpainless\-cli\-inline\-sign \- Create an inline\-signed message from data on standard input .SH "SYNOPSIS" .sp -\fBpgpainless\-cli inline\-sign\fP [\fB\-\-[no\-]armor\fP] [\fB\-\-stacktrace\fP] [\fB\-\-as\fP= -\fI{binary|text|cleartextsigned}\fP] +\fBpgpainless\-cli inline\-sign\fP [\fB\-\-[no\-]armor\fP] [\fB\-\-stacktrace\fP] [\fB\-\-as\fP=\fI{binary|text|clearsigned}\fP] [\fB\-\-with\-key\-password\fP=\fIPASSWORD\fP]... [\fIKEYS\fP...] .SH "DESCRIPTION" .SH "OPTIONS" .sp -\fB\-\-as\fP=\fI{binary|text|cleartextsigned}\fP +\fB\-\-as\fP=\fI{binary|text|clearsigned}\fP .RS 4 -Specify the signature format of the signed message +Specify the signature format of the signed message. .sp \(aqtext\(aq and \(aqbinary\(aq will produce inline\-signed messages. .sp -\(aqcleartextsigned\(aq will make use of the cleartext signature framework. +\(aqclearsigned\(aq will make use of the cleartext signature framework. .sp Defaults to \(aqbinary\(aq. .sp diff --git a/pgpainless-cli/packaging/man/pgpainless-cli-list-profiles.1 b/pgpainless-cli/packaging/man/pgpainless-cli-list-profiles.1 new file mode 100644 index 00000000..9bcfa17f --- /dev/null +++ b/pgpainless-cli/packaging/man/pgpainless-cli-list-profiles.1 @@ -0,0 +1,46 @@ +'\" t +.\" Title: pgpainless-cli-list-profiles +.\" Author: [see the "AUTHOR(S)" section] +.\" Generator: Asciidoctor 2.0.10 +.\" Manual: PGPainless-CLI Manual +.\" Source: +.\" Language: English +.\" +.TH "PGPAINLESS\-CLI\-LIST\-PROFILES" "1" "" "" "PGPainless\-CLI Manual" +.ie \n(.g .ds Aq \(aq +.el .ds Aq ' +.ss \n[.ss] 0 +.nh +.ad l +.de URL +\fI\\$2\fP <\\$1>\\$3 +.. +.als MTO URL +.if \n[.g] \{\ +. mso www.tmac +. am URL +. ad l +. . +. am MTO +. ad l +. . +. LINKSTYLE blue R < > +.\} +.SH "NAME" +pgpainless\-cli\-list\-profiles \- Emit a list of profiles supported by the identified subcommand +.SH "SYNOPSIS" +.sp +\fBpgpainless\-cli list\-profiles\fP [\fB\-\-stacktrace\fP] \fICOMMAND\fP +.SH "DESCRIPTION" + +.SH "OPTIONS" +.sp +\fB\-\-stacktrace\fP +.RS 4 +.RE +.SH "ARGUMENTS" +.sp +\fICOMMAND\fP +.RS 4 +Subcommand for which to list profiles +.RE \ No newline at end of file diff --git a/pgpainless-cli/packaging/man/pgpainless-cli-sign.1 b/pgpainless-cli/packaging/man/pgpainless-cli-sign.1 index 6519e0ec..5bb22e90 100644 --- a/pgpainless-cli/packaging/man/pgpainless-cli-sign.1 +++ b/pgpainless-cli/packaging/man/pgpainless-cli-sign.1 @@ -38,7 +38,7 @@ pgpainless\-cli\-sign \- Create a detached signature on the data from standard i .sp \fB\-\-as\fP=\fI{binary|text}\fP .RS 4 -Specify the output format of the signed message +Specify the output format of the signed message. .sp Defaults to \(aqbinary\(aq. .sp @@ -47,7 +47,7 @@ If \(aq\-\-as=text\(aq and the input data is not valid UTF\-8, sign fails with r .sp \fB\-\-micalg\-out\fP=\fIMICALG\fP .RS 4 -Emits the digest algorithm used to the specified file in a way that can be used to populate the micalg parameter for the PGP/MIME Content\-Type (RFC3156) +Emits the digest algorithm used to the specified file in a way that can be used to populate the micalg parameter for the PGP/MIME Content\-Type (RFC3156). .RE .sp \fB\-\-[no\-]armor\fP diff --git a/pgpainless-cli/packaging/man/pgpainless-cli-verify.1 b/pgpainless-cli/packaging/man/pgpainless-cli-verify.1 index 5cf0020c..714064f6 100644 --- a/pgpainless-cli/packaging/man/pgpainless-cli-verify.1 +++ b/pgpainless-cli/packaging/man/pgpainless-cli-verify.1 @@ -36,7 +36,18 @@ pgpainless\-cli\-verify \- Verify a detached signature over the data from standa .SH "OPTIONS" .sp -\fB\-\-not\-after\fP=\fIDATE\fP, \fB\-\-not\-before\fP=\fIDATE\fP +\fB\-\-not\-after\fP=\fIDATE\fP +.RS 4 +ISO\-8601 formatted UTC date (e.g. \(aq2020\-11\-23T16:35Z) +.sp +Reject signatures with a creation date not in range. +.sp +Defaults to current system time ("now"). +.sp +Accepts special value "\-" for end of time. +.RE +.sp +\fB\-\-not\-before\fP=\fIDATE\fP .RS 4 ISO\-8601 formatted UTC date (e.g. \(aq2020\-11\-23T16:35Z) .sp diff --git a/pgpainless-cli/packaging/man/pgpainless-cli-version.1 b/pgpainless-cli/packaging/man/pgpainless-cli-version.1 index 003e549f..f1bea312 100644 --- a/pgpainless-cli/packaging/man/pgpainless-cli-version.1 +++ b/pgpainless-cli/packaging/man/pgpainless-cli-version.1 @@ -30,7 +30,7 @@ pgpainless\-cli\-version \- Display version information about the tool .SH "SYNOPSIS" .sp -\fBpgpainless\-cli version\fP [\fB\-\-stacktrace\fP] [\fB\-\-extended\fP | \fB\-\-backend\fP] +\fBpgpainless\-cli version\fP [\fB\-\-stacktrace\fP] [\fB\-\-extended\fP | \fB\-\-backend\fP | \fB\-\-pgpainless\-cli\-spec\fP] .SH "DESCRIPTION" .SH "OPTIONS" @@ -45,6 +45,11 @@ Print information about the cryptographic backend Print an extended version string .RE .sp +\fB\-\-pgpainless\-cli\-spec\fP +.RS 4 +Print the latest revision of the SOP specification targeted by the implementation +.RE +.sp \fB\-\-stacktrace\fP .RS 4 .RE \ No newline at end of file diff --git a/pgpainless-cli/packaging/man/pgpainless-cli.1 b/pgpainless-cli/packaging/man/pgpainless-cli.1 index 686f728f..e5cc8129 100644 --- a/pgpainless-cli/packaging/man/pgpainless-cli.1 +++ b/pgpainless-cli/packaging/man/pgpainless-cli.1 @@ -101,6 +101,11 @@ Create an inline\-signed message from data on standard input Verify inline\-signed data from standard input .RE .sp +\fBlist\-profiles\fP +.RS 4 +Emit a list of profiles supported by the identified subcommand +.RE +.sp \fBversion\fP .RS 4 Display version information about the tool @@ -205,4 +210,14 @@ Ambiguous input (a filename matching the designator already exists) \fB79\fP .RS 4 Key is not signing capable +.RE +.sp +\fB83\fP +.RS 4 +Options were supplied that are incompatible with each other +.RE +.sp +\fB89\fP +.RS 4 +The requested profile is unsupported, or the indicated subcommand does not accept profiles .RE \ No newline at end of file From 52fa7e4d46331bacb6839b73012d612d147d6bfa Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 1 May 2023 09:35:28 +0200 Subject: [PATCH 278/763] OpenPgpMessageInputStream: Return -1 instead of throwing MalformedOpenPgpMessageException when calling read() on drained stream --- .../OpenPgpMessageInputStream.java | 2 ++ .../syntax_check/OpenPgpMessageSyntax.java | 4 ++++ .../OpenPgpMessageInputStreamTest.java | 17 +++++++++++++++++ 3 files changed, 23 insertions(+) diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java index 19a01fbf..d51a6cf7 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java @@ -744,6 +744,7 @@ public class OpenPgpMessageInputStream extends DecryptionStream { throws IOException { if (nestedInputStream == null) { if (packetInputStream != null) { + syntaxVerifier.next(InputSymbol.EndOfSequence); syntaxVerifier.assertValid(); } return -1; @@ -774,6 +775,7 @@ public class OpenPgpMessageInputStream extends DecryptionStream { super.close(); if (closed) { if (packetInputStream != null) { + syntaxVerifier.next(InputSymbol.EndOfSequence); syntaxVerifier.assertValid(); } return; diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/OpenPgpMessageSyntax.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/OpenPgpMessageSyntax.java index 6abb507a..9d20e0a8 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/OpenPgpMessageSyntax.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/OpenPgpMessageSyntax.java @@ -132,6 +132,10 @@ public class OpenPgpMessageSyntax implements Syntax { @Nonnull Transition fromValid(@Nonnull InputSymbol input, @Nullable StackSymbol stackItem) throws MalformedOpenPgpMessageException { + if (input == InputSymbol.EndOfSequence) { + // allow subsequent read() calls. + return new Transition(State.Valid); + } // There is no applicable transition rule out of Valid throw new MalformedOpenPgpMessageException(State.Valid, input, stackItem); } diff --git a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStreamTest.java b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStreamTest.java index 01966bbe..a0ec7c25 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStreamTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStreamTest.java @@ -34,6 +34,7 @@ import org.bouncycastle.openpgp.PGPSignature; import org.bouncycastle.util.io.Streams; import org.junit.JUtils; import org.junit.jupiter.api.Named; +import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; @@ -652,6 +653,22 @@ public class OpenPgpMessageInputStreamTest { assertTrue(metadata.getRejectedInlineSignatures().isEmpty()); } + @Test + public void readAfterCloseTest() throws PGPException, IOException { + OpenPgpMessageInputStream pgpIn = get(SENC_LIT, ConsumerOptions.get() + .addDecryptionPassphrase(Passphrase.fromPassword(PASSPHRASE))); + Streams.drain(pgpIn); // read all + + byte[] buf = new byte[1024]; + assertEquals(-1, pgpIn.read(buf)); + assertEquals(-1, pgpIn.read()); + assertEquals(-1, pgpIn.read(buf)); + assertEquals(-1, pgpIn.read()); + + pgpIn.close(); + pgpIn.getMetadata(); + } + private static Tuple processReadBuffered(String armoredMessage, ConsumerOptions options) throws PGPException, IOException { OpenPgpMessageInputStream in = get(armoredMessage, options); From 2e730a2c4873b01940bee213dd4a4d8390f2d223 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 1 May 2023 10:10:50 +0200 Subject: [PATCH 279/763] Update changelog --- CHANGELOG.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 506ded3a..59868c3a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,8 +5,11 @@ SPDX-License-Identifier: CC0-1.0 # PGPainless Changelog -## 1.5.2-SNAPSHOT +## 1.5.2-rc1 - Bump `sop-java` to `6.1.0` +- Normalize `OpenPgpMessageInputStream.read()` behaviour when reading past the stream + - Instead of throwing a `MalformedOpenPgpMessageException` which could throw off unsuspecting parsers, + we now simply return `-1` like every other `InputStream`. ## 1.5.1 - SOP: Emit signature `mode:{binary|text}` in `Verification` results From de5926fc472976b7a9fe24f0568364c51204c7f8 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 1 May 2023 10:12:37 +0200 Subject: [PATCH 280/763] PGPainless 1.5.2-rc1 --- README.md | 2 +- pgpainless-sop/README.md | 4 ++-- version.gradle | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 06e9f2ed..903d21b8 100644 --- a/README.md +++ b/README.md @@ -191,7 +191,7 @@ repositories { } dependencies { - implementation 'org.pgpainless:pgpainless-core:1.5.1' + implementation 'org.pgpainless:pgpainless-core:1.5.2-rc1' } ``` diff --git a/pgpainless-sop/README.md b/pgpainless-sop/README.md index 7ae0a63c..a082f09c 100644 --- a/pgpainless-sop/README.md +++ b/pgpainless-sop/README.md @@ -23,7 +23,7 @@ To start using pgpainless-sop in your code, include the following lines in your ... dependencies { ... - implementation "org.pgpainless:pgpainless-sop:1.5.1" + implementation "org.pgpainless:pgpainless-sop:1.5.2-rc1" ... } @@ -34,7 +34,7 @@ dependencies { org.pgpainless pgpainless-sop - 1.5.1 + 1.5.2-rc1 ... diff --git a/version.gradle b/version.gradle index ce7eebc7..46a173b3 100644 --- a/version.gradle +++ b/version.gradle @@ -4,8 +4,8 @@ allprojects { ext { - shortVersion = '1.5.2' - isSnapshot = true + shortVersion = '1.5.2-rc1' + isSnapshot = false pgpainlessMinAndroidSdk = 10 javaSourceCompatibility = 1.8 bouncyCastleVersion = '1.73' From 671d45a9116e36a3124610e735c3fb122968923b Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 1 May 2023 10:15:24 +0200 Subject: [PATCH 281/763] PGPainless 1.5.2-rc2-SNAPSHOT --- version.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/version.gradle b/version.gradle index 46a173b3..564100d4 100644 --- a/version.gradle +++ b/version.gradle @@ -4,8 +4,8 @@ allprojects { ext { - shortVersion = '1.5.2-rc1' - isSnapshot = false + shortVersion = '1.5.2-rc2' + isSnapshot = true pgpainlessMinAndroidSdk = 10 javaSourceCompatibility = 1.8 bouncyCastleVersion = '1.73' From 9c81137f4884c9d401dbc0435d40ba872dcb704e Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 3 May 2023 13:51:34 +0200 Subject: [PATCH 282/763] Add template methods to generate RSA keys with primary and subkeys --- .../key/generation/KeyRingTemplates.java | 72 +++++++++++++++++++ 1 file changed, 72 insertions(+) diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/generation/KeyRingTemplates.java b/pgpainless-core/src/main/java/org/pgpainless/key/generation/KeyRingTemplates.java index 42eb7efa..07f2235f 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/key/generation/KeyRingTemplates.java +++ b/pgpainless-core/src/main/java/org/pgpainless/key/generation/KeyRingTemplates.java @@ -26,6 +26,78 @@ public final class KeyRingTemplates { } + /** + * Generate an RSA OpenPGP key consisting of an RSA primary key used for certification, + * a dedicated RSA subkey used for signing and a third RSA subkey used for encryption. + * + * @param userId userId or null + * @param length length of the RSA keys + * @return key + * @throws InvalidAlgorithmParameterException in case of invalid key generation parameters + * @throws NoSuchAlgorithmException in case of missing algorithm implementation in the crypto provider + * @throws PGPException in case of an OpenPGP related error + */ + public PGPSecretKeyRing rsaKeyRing(@Nullable CharSequence userId, + @Nonnull RsaLength length) + throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException { + return rsaKeyRing(userId, length, Passphrase.emptyPassphrase()); + } + + /** + * Generate an RSA OpenPGP key consisting of an RSA primary key used for certification, + * a dedicated RSA subkey used for signing and a third RSA subkey used for encryption. + * + * @param userId userId or null + * @param length length of the RSA keys + * @param password passphrase to encrypt the key with + * @return key + * @throws InvalidAlgorithmParameterException in case of invalid key generation parameters + * @throws NoSuchAlgorithmException in case of missing algorithm implementation in the crypto provider + * @throws PGPException in case of an OpenPGP related error + */ + public PGPSecretKeyRing rsaKeyRing(@Nullable CharSequence userId, + @Nonnull RsaLength length, + @Nonnull String password) + throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException { + Passphrase passphrase = Passphrase.emptyPassphrase(); + if (!isNullOrEmpty(password)) { + passphrase = Passphrase.fromPassword(password); + } + return rsaKeyRing(userId, length, passphrase); + } + + /** + * Generate an RSA OpenPGP key consisting of an RSA primary key used for certification, + * a dedicated RSA subkey used for signing and a third RSA subkey used for encryption. + * + * @param userId userId or null + * @param length length of the RSA keys + * @param passphrase passphrase to encrypt the key with + * @return key + * @throws InvalidAlgorithmParameterException in case of invalid key generation parameters + * @throws NoSuchAlgorithmException in case of missing algorithm implementation in the crypto provider + * @throws PGPException in case of an OpenPGP related error + */ + public PGPSecretKeyRing rsaKeyRing(@Nullable CharSequence userId, + @Nonnull RsaLength length, + @Nonnull Passphrase passphrase) + throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException { + KeyRingBuilder builder = PGPainless.buildKeyRing() + .setPrimaryKey(KeySpec.getBuilder(KeyType.RSA(length), KeyFlag.CERTIFY_OTHER)) + .addSubkey(KeySpec.getBuilder(KeyType.RSA(length), KeyFlag.SIGN_DATA)) + .addSubkey(KeySpec.getBuilder(KeyType.RSA(length), KeyFlag.ENCRYPT_COMMS, KeyFlag.ENCRYPT_STORAGE)); + + if (userId != null) { + builder.addUserId(userId.toString()); + } + + if (!passphrase.isEmpty()) { + builder.setPassphrase(passphrase); + } + + return builder.build(); + } + /** * Creates a simple, unencrypted RSA KeyPair of length {@code length} with user-id {@code userId}. * The KeyPair consists of a single RSA master key which is used for signing, encryption and certification. From 8869d9bd783ead32c70725f7dd1cd17d070f085f Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 3 May 2023 13:51:59 +0200 Subject: [PATCH 283/763] Simplify key template methods by replacing String and UserID args with CharSequence --- .../key/generation/KeyRingTemplates.java | 113 +++--------------- 1 file changed, 19 insertions(+), 94 deletions(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/generation/KeyRingTemplates.java b/pgpainless-core/src/main/java/org/pgpainless/key/generation/KeyRingTemplates.java index 07f2235f..6966232b 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/key/generation/KeyRingTemplates.java +++ b/pgpainless-core/src/main/java/org/pgpainless/key/generation/KeyRingTemplates.java @@ -17,7 +17,6 @@ import org.pgpainless.key.generation.type.KeyType; import org.pgpainless.key.generation.type.eddsa.EdDSACurve; import org.pgpainless.key.generation.type.rsa.RsaLength; import org.pgpainless.key.generation.type.xdh.XDHSpec; -import org.pgpainless.key.util.UserId; import org.pgpainless.util.Passphrase; public final class KeyRingTemplates { @@ -111,46 +110,20 @@ public final class KeyRingTemplates { * @throws NoSuchAlgorithmException in case of missing algorithm implementation in the crypto provider * @throws PGPException in case of an OpenPGP related error */ - public PGPSecretKeyRing simpleRsaKeyRing(@Nullable UserId userId, @Nonnull RsaLength length) - throws InvalidAlgorithmParameterException, NoSuchAlgorithmException, PGPException { - return simpleRsaKeyRing(userId == null ? null : userId.toString(), length); - } - - /** - * Creates a simple, unencrypted RSA KeyPair of length {@code length} with user-id {@code userId}. - * The KeyPair consists of a single RSA master key which is used for signing, encryption and certification. - * - * @param userId user id. - * @param length length in bits. - * - * @return {@link PGPSecretKeyRing} containing the KeyPair. - * - * @throws InvalidAlgorithmParameterException in case of invalid key generation parameters - * @throws NoSuchAlgorithmException in case of missing algorithm implementation in the crypto provider - * @throws PGPException in case of an OpenPGP related error - */ - public PGPSecretKeyRing simpleRsaKeyRing(@Nullable String userId, @Nonnull RsaLength length) + public PGPSecretKeyRing simpleRsaKeyRing(@Nullable CharSequence userId, @Nonnull RsaLength length) throws InvalidAlgorithmParameterException, NoSuchAlgorithmException, PGPException { return simpleRsaKeyRing(userId, length, Passphrase.emptyPassphrase()); } - /** - * Creates a simple RSA KeyPair of length {@code length} with user-id {@code userId}. - * The KeyPair consists of a single RSA master key which is used for signing, encryption and certification. - * - * @param userId user id. - * @param length length in bits. - * @param password Password of the key. Can be null for unencrypted keys. - * - * @return {@link PGPSecretKeyRing} containing the KeyPair. - * - * @throws InvalidAlgorithmParameterException in case of invalid key generation parameters - * @throws NoSuchAlgorithmException in case of missing algorithm implementation in the crypto provider - * @throws PGPException in case of an OpenPGP related error - */ - public PGPSecretKeyRing simpleRsaKeyRing(@Nullable UserId userId, @Nonnull RsaLength length, @Nullable String password) - throws InvalidAlgorithmParameterException, NoSuchAlgorithmException, PGPException { - return simpleRsaKeyRing(userId == null ? null : userId.toString(), length, password); + public PGPSecretKeyRing simpleRsaKeyRing(@Nullable CharSequence userId, @Nonnull RsaLength length, @Nonnull Passphrase passphrase) + throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException { + KeyRingBuilder builder = PGPainless.buildKeyRing() + .setPrimaryKey(KeySpec.getBuilder(KeyType.RSA(length), KeyFlag.CERTIFY_OTHER, KeyFlag.SIGN_DATA, KeyFlag.ENCRYPT_COMMS)) + .setPassphrase(passphrase); + if (userId != null) { + builder.addUserId(userId.toString()); + } + return builder.build(); } /** @@ -167,7 +140,7 @@ public final class KeyRingTemplates { * @throws NoSuchAlgorithmException in case of missing algorithm implementation in the crypto provider * @throws PGPException in case of an OpenPGP related error */ - public PGPSecretKeyRing simpleRsaKeyRing(@Nullable String userId, @Nonnull RsaLength length, @Nullable String password) + public PGPSecretKeyRing simpleRsaKeyRing(@Nullable CharSequence userId, @Nonnull RsaLength length, @Nullable String password) throws PGPException, NoSuchAlgorithmException, InvalidAlgorithmParameterException { Passphrase passphrase = Passphrase.emptyPassphrase(); if (!isNullOrEmpty(password)) { @@ -176,17 +149,6 @@ public final class KeyRingTemplates { return simpleRsaKeyRing(userId, length, passphrase); } - public PGPSecretKeyRing simpleRsaKeyRing(@Nullable String userId, @Nonnull RsaLength length, @Nonnull Passphrase passphrase) - throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException { - KeyRingBuilder builder = PGPainless.buildKeyRing() - .setPrimaryKey(KeySpec.getBuilder(KeyType.RSA(length), KeyFlag.CERTIFY_OTHER, KeyFlag.SIGN_DATA, KeyFlag.ENCRYPT_COMMS)) - .setPassphrase(passphrase); - if (userId != null) { - builder.addUserId(userId); - } - return builder.build(); - } - /** * Creates a key ring consisting of an ed25519 EdDSA primary key and a curve25519 XDH subkey. * The EdDSA primary key is used for signing messages and certifying the sub key. @@ -200,48 +162,11 @@ public final class KeyRingTemplates { * @throws NoSuchAlgorithmException in case of missing algorithm implementation in the crypto provider * @throws PGPException in case of an OpenPGP related error */ - public PGPSecretKeyRing simpleEcKeyRing(@Nullable UserId userId) - throws InvalidAlgorithmParameterException, NoSuchAlgorithmException, PGPException { - return simpleEcKeyRing(userId == null ? null : userId.toString()); - } - - /** - * Creates a key ring consisting of an ed25519 EdDSA primary key and a curve25519 XDH subkey. - * The EdDSA primary key is used for signing messages and certifying the sub key. - * The XDH subkey is used for encryption and decryption of messages. - * - * @param userId user-id - * - * @return {@link PGPSecretKeyRing} containing the key pairs. - * - * @throws InvalidAlgorithmParameterException in case of invalid key generation parameters - * @throws NoSuchAlgorithmException in case of missing algorithm implementation in the crypto provider - * @throws PGPException in case of an OpenPGP related error - */ - public PGPSecretKeyRing simpleEcKeyRing(@Nullable String userId) + public PGPSecretKeyRing simpleEcKeyRing(@Nullable CharSequence userId) throws InvalidAlgorithmParameterException, NoSuchAlgorithmException, PGPException { return simpleEcKeyRing(userId, Passphrase.emptyPassphrase()); } - /** - * Creates a key ring consisting of an ed25519 EdDSA primary key and a curve25519 XDH subkey. - * The EdDSA primary key is used for signing messages and certifying the sub key. - * The XDH subkey is used for encryption and decryption of messages. - * - * @param userId user-id - * @param password Password of the private key. Can be null for an unencrypted key. - * - * @return {@link PGPSecretKeyRing} containing the key pairs. - * - * @throws InvalidAlgorithmParameterException in case of invalid key generation parameters - * @throws NoSuchAlgorithmException in case of missing algorithm implementation in the crypto provider - * @throws PGPException in case of an OpenPGP related error - */ - public PGPSecretKeyRing simpleEcKeyRing(@Nullable UserId userId, String password) - throws InvalidAlgorithmParameterException, NoSuchAlgorithmException, PGPException { - return simpleEcKeyRing(userId == null ? null : userId.toString(), password); - } - /** * Creates a key ring consisting of an ed25519 EdDSA primary key and a X25519 XDH subkey. * The EdDSA primary key is used for signing messages and certifying the sub key. @@ -256,7 +181,7 @@ public final class KeyRingTemplates { * @throws NoSuchAlgorithmException in case of missing algorithm implementation in the crypto provider * @throws PGPException in case of an OpenPGP related error */ - public PGPSecretKeyRing simpleEcKeyRing(@Nullable String userId, String password) + public PGPSecretKeyRing simpleEcKeyRing(@Nullable CharSequence userId, String password) throws PGPException, NoSuchAlgorithmException, InvalidAlgorithmParameterException { Passphrase passphrase = Passphrase.emptyPassphrase(); if (!isNullOrEmpty(password)) { @@ -265,14 +190,14 @@ public final class KeyRingTemplates { return simpleEcKeyRing(userId, passphrase); } - public PGPSecretKeyRing simpleEcKeyRing(@Nullable String userId, @Nonnull Passphrase passphrase) + public PGPSecretKeyRing simpleEcKeyRing(@Nullable CharSequence userId, @Nonnull Passphrase passphrase) throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException { KeyRingBuilder builder = PGPainless.buildKeyRing() .setPrimaryKey(KeySpec.getBuilder(KeyType.EDDSA(EdDSACurve._Ed25519), KeyFlag.CERTIFY_OTHER, KeyFlag.SIGN_DATA)) .addSubkey(KeySpec.getBuilder(KeyType.XDH(XDHSpec._X25519), KeyFlag.ENCRYPT_STORAGE, KeyFlag.ENCRYPT_COMMS)) .setPassphrase(passphrase); if (userId != null) { - builder.addUserId(userId); + builder.addUserId(userId.toString()); } return builder.build(); } @@ -288,7 +213,7 @@ public final class KeyRingTemplates { * @throws NoSuchAlgorithmException in case of missing algorithm implementation in the crypto provider * @throws PGPException in case of an OpenPGP related error */ - public PGPSecretKeyRing modernKeyRing(@Nullable String userId) throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException { + public PGPSecretKeyRing modernKeyRing(@Nullable CharSequence userId) throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException { return modernKeyRing(userId, Passphrase.emptyPassphrase()); } @@ -304,13 +229,13 @@ public final class KeyRingTemplates { * @throws NoSuchAlgorithmException in case of missing algorithm implementation in the crypto provider * @throws PGPException in case of an OpenPGP related error */ - public PGPSecretKeyRing modernKeyRing(@Nullable String userId, @Nullable String password) + public PGPSecretKeyRing modernKeyRing(@Nullable CharSequence userId, @Nullable String password) throws InvalidAlgorithmParameterException, NoSuchAlgorithmException, PGPException { Passphrase passphrase = (password != null ? Passphrase.fromPassword(password) : Passphrase.emptyPassphrase()); return modernKeyRing(userId, passphrase); } - public PGPSecretKeyRing modernKeyRing(@Nullable String userId, @Nonnull Passphrase passphrase) + public PGPSecretKeyRing modernKeyRing(@Nullable CharSequence userId, @Nonnull Passphrase passphrase) throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException { KeyRingBuilder builder = PGPainless.buildKeyRing() .setPrimaryKey(KeySpec.getBuilder(KeyType.EDDSA(EdDSACurve._Ed25519), KeyFlag.CERTIFY_OTHER)) @@ -318,7 +243,7 @@ public final class KeyRingTemplates { .addSubkey(KeySpec.getBuilder(KeyType.EDDSA(EdDSACurve._Ed25519), KeyFlag.SIGN_DATA)) .setPassphrase(passphrase); if (userId != null) { - builder.addUserId(userId); + builder.addUserId(userId.toString()); } return builder.build(); } From a8ab93a49a3faff3f0c9bd7d3074e53fa0f46cee Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 3 May 2023 14:07:33 +0200 Subject: [PATCH 284/763] SOP: GenerateKey with --profile=rfc4880 now generates RSA key with subkeys --- .../src/main/java/org/pgpainless/sop/GenerateKeyImpl.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/GenerateKeyImpl.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/GenerateKeyImpl.java index ba788dac..f86d2893 100644 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/GenerateKeyImpl.java +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/GenerateKeyImpl.java @@ -126,7 +126,7 @@ public class GenerateKeyImpl implements GenerateKey { // RSA 4096 else if (profile.equals(RSA4096_PROFILE.getName())) { key = PGPainless.generateKeyRing() - .simpleRsaKeyRing(primaryUserId, RsaLength._4096, passphrase); + .rsaKeyRing(primaryUserId, RsaLength._4096, passphrase); } else { // Missing else-if branch for profile. Oops. From 15f6cc70b1979882a0ccb0b68db4c0c32e534a86 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 3 May 2023 14:30:08 +0200 Subject: [PATCH 285/763] Add MessageMetadata.getRecipientKeyIds() Fixes #376 --- .../decryption_verification/MessageMetadata.java | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/MessageMetadata.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/MessageMetadata.java index 648b99ab..cc97e81a 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/MessageMetadata.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/MessageMetadata.java @@ -92,6 +92,21 @@ public class MessageMetadata { return false; } + /** + * Return a list containing all recipient keyIDs. + * + * @return list of recipients + */ + public List getRecipientKeyIds() { + List keyIds = new ArrayList<>(); + Iterator encLayers = getEncryptionLayers(); + while (encLayers.hasNext()) { + EncryptedData layer = encLayers.next(); + keyIds.addAll(layer.getRecipients()); + } + return keyIds; + } + public @Nonnull Iterator getEncryptionLayers() { return new LayerIterator(message) { @Override From 304350fe5c7b771fb6ab3f8a0dd17cbbd5efd0c2 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 3 May 2023 14:38:38 +0200 Subject: [PATCH 286/763] Add p-tags to EncryptionOptions javadoc --- .../pgpainless/encryption_signing/EncryptionOptions.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/EncryptionOptions.java b/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/EncryptionOptions.java index b5baee83..2d0fb156 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/EncryptionOptions.java +++ b/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/EncryptionOptions.java @@ -33,7 +33,7 @@ import org.pgpainless.util.Passphrase; /** * Options for the encryption process. * This class can be used to set encryption parameters, like encryption keys and passphrases, algorithms etc. - * + *

      * A typical use might look like follows: *

        * {@code
      @@ -42,11 +42,11 @@ import org.pgpainless.util.Passphrase;
        * opt.addPassphrase(Passphrase.fromPassword("AdditionalDecryptionPassphrase123"));
        * }
        * 
      - * + *

      * To use a custom symmetric encryption algorithm, use {@link #overrideEncryptionAlgorithm(SymmetricKeyAlgorithm)}. * This will cause PGPainless to use the provided algorithm for message encryption, instead of negotiating an algorithm * by inspecting the provided recipient keys. - * + *

      * By default, PGPainless will encrypt to all suitable, encryption capable subkeys on each recipient's certificate. * This behavior can be changed per recipient, e.g. by calling *

      @@ -83,7 +83,7 @@ public class EncryptionOptions {
            * Factory method to create an {@link EncryptionOptions} object which will encrypt for keys
            * which carry either the {@link org.pgpainless.algorithm.KeyFlag#ENCRYPT_COMMS} or
            * {@link org.pgpainless.algorithm.KeyFlag#ENCRYPT_STORAGE} flag.
      -     *
      +     * 

      * Use this if you are not sure. * * @return encryption options From 64c6d7a90409c57837e47cb5ee1eb4f95c73a5a4 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 3 May 2023 14:38:52 +0200 Subject: [PATCH 287/763] Annotate EncryptionOptions methods with @Nonnull --- .../encryption_signing/EncryptionOptions.java | 34 +++++++++++-------- .../EncryptionOptionsTest.java | 6 ++-- 2 files changed, 23 insertions(+), 17 deletions(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/EncryptionOptions.java b/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/EncryptionOptions.java index 2d0fb156..63320853 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/EncryptionOptions.java +++ b/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/EncryptionOptions.java @@ -75,7 +75,7 @@ public class EncryptionOptions { this(EncryptionPurpose.ANY); } - public EncryptionOptions(EncryptionPurpose purpose) { + public EncryptionOptions(@Nonnull EncryptionPurpose purpose) { this.purpose = purpose; } @@ -118,7 +118,7 @@ public class EncryptionOptions { * @param keys keys * @return this */ - public EncryptionOptions addRecipients(Iterable keys) { + public EncryptionOptions addRecipients(@Nonnull Iterable keys) { if (!keys.iterator().hasNext()) { throw new IllegalArgumentException("Set of recipient keys cannot be empty."); } @@ -154,7 +154,7 @@ public class EncryptionOptions { * @param userId user id * @return this */ - public EncryptionOptions addRecipient(PGPPublicKeyRing key, String userId) { + public EncryptionOptions addRecipient(@Nonnull PGPPublicKeyRing key, @Nonnull CharSequence userId) { return addRecipient(key, userId, encryptionKeySelector); } @@ -167,11 +167,13 @@ public class EncryptionOptions { * @param encryptionKeySelectionStrategy strategy to select one or more encryption subkeys to encrypt to * @return this */ - public EncryptionOptions addRecipient(PGPPublicKeyRing key, String userId, EncryptionKeySelector encryptionKeySelectionStrategy) { + public EncryptionOptions addRecipient(@Nonnull PGPPublicKeyRing key, + @Nonnull CharSequence userId, + @Nonnull EncryptionKeySelector encryptionKeySelectionStrategy) { KeyRingInfo info = new KeyRingInfo(key, new Date()); List encryptionSubkeys = encryptionKeySelectionStrategy - .selectEncryptionSubkeys(info.getEncryptionSubkeys(userId, purpose)); + .selectEncryptionSubkeys(info.getEncryptionSubkeys(userId.toString(), purpose)); if (encryptionSubkeys.isEmpty()) { throw new KeyException.UnacceptableEncryptionKeyException(OpenPgpFingerprint.of(key)); } @@ -179,7 +181,7 @@ public class EncryptionOptions { for (PGPPublicKey encryptionSubkey : encryptionSubkeys) { SubkeyIdentifier keyId = new SubkeyIdentifier(key, encryptionSubkey.getKeyID()); keyRingInfo.put(keyId, info); - keyViews.put(keyId, new KeyAccessor.ViaUserId(info, keyId, userId)); + keyViews.put(keyId, new KeyAccessor.ViaUserId(info, keyId, userId.toString())); addRecipientKey(key, encryptionSubkey); } @@ -192,7 +194,7 @@ public class EncryptionOptions { * @param key key ring * @return this */ - public EncryptionOptions addRecipient(PGPPublicKeyRing key) { + public EncryptionOptions addRecipient(@Nonnull PGPPublicKeyRing key) { return addRecipient(key, encryptionKeySelector); } @@ -203,7 +205,8 @@ public class EncryptionOptions { * @param encryptionKeySelectionStrategy strategy used to select one or multiple encryption subkeys. * @return this */ - public EncryptionOptions addRecipient(PGPPublicKeyRing key, EncryptionKeySelector encryptionKeySelectionStrategy) { + public EncryptionOptions addRecipient(@Nonnull PGPPublicKeyRing key, + @Nonnull EncryptionKeySelector encryptionKeySelectionStrategy) { Date evaluationDate = new Date(); KeyRingInfo info; info = new KeyRingInfo(key, evaluationDate); @@ -234,7 +237,8 @@ public class EncryptionOptions { return this; } - private void addRecipientKey(PGPPublicKeyRing keyRing, PGPPublicKey key) { + private void addRecipientKey(@Nonnull PGPPublicKeyRing keyRing, + @Nonnull PGPPublicKey key) { encryptionKeys.add(new SubkeyIdentifier(keyRing, key.getKeyID())); PGPKeyEncryptionMethodGenerator encryptionMethod = ImplementationFactory .getInstance().getPublicKeyKeyEncryptionMethodGenerator(key); @@ -247,7 +251,7 @@ public class EncryptionOptions { * @param passphrase passphrase * @return this */ - public EncryptionOptions addPassphrase(Passphrase passphrase) { + public EncryptionOptions addPassphrase(@Nonnull Passphrase passphrase) { if (passphrase.isEmpty()) { throw new IllegalArgumentException("Passphrase must not be empty."); } @@ -267,7 +271,7 @@ public class EncryptionOptions { * @param encryptionMethod encryption method * @return this */ - public EncryptionOptions addEncryptionMethod(PGPKeyEncryptionMethodGenerator encryptionMethod) { + public EncryptionOptions addEncryptionMethod(@Nonnull PGPKeyEncryptionMethodGenerator encryptionMethod) { encryptionMethods.add(encryptionMethod); return this; } @@ -303,7 +307,7 @@ public class EncryptionOptions { * @param encryptionAlgorithm encryption algorithm override * @return this */ - public EncryptionOptions overrideEncryptionAlgorithm(SymmetricKeyAlgorithm encryptionAlgorithm) { + public EncryptionOptions overrideEncryptionAlgorithm(@Nonnull SymmetricKeyAlgorithm encryptionAlgorithm) { if (encryptionAlgorithm == SymmetricKeyAlgorithm.NULL) { throw new IllegalArgumentException("Plaintext encryption can only be used to denote unencrypted secret keys."); } @@ -322,7 +326,7 @@ public class EncryptionOptions { } public interface EncryptionKeySelector { - List selectEncryptionSubkeys(List encryptionCapableKeys); + List selectEncryptionSubkeys(@Nonnull List encryptionCapableKeys); } /** @@ -333,7 +337,7 @@ public class EncryptionOptions { public static EncryptionKeySelector encryptToFirstSubkey() { return new EncryptionKeySelector() { @Override - public List selectEncryptionSubkeys(List encryptionCapableKeys) { + public List selectEncryptionSubkeys(@Nonnull List encryptionCapableKeys) { return encryptionCapableKeys.isEmpty() ? Collections.emptyList() : Collections.singletonList(encryptionCapableKeys.get(0)); } }; @@ -347,7 +351,7 @@ public class EncryptionOptions { public static EncryptionKeySelector encryptToAllCapableSubkeys() { return new EncryptionKeySelector() { @Override - public List selectEncryptionSubkeys(List encryptionCapableKeys) { + public List selectEncryptionSubkeys(@Nonnull List encryptionCapableKeys) { return encryptionCapableKeys; } }; diff --git a/pgpainless-core/src/test/java/org/pgpainless/encryption_signing/EncryptionOptionsTest.java b/pgpainless-core/src/test/java/org/pgpainless/encryption_signing/EncryptionOptionsTest.java index 3436ba69..7d2fa453 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/encryption_signing/EncryptionOptionsTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/encryption_signing/EncryptionOptionsTest.java @@ -36,6 +36,8 @@ import org.pgpainless.key.generation.type.xdh.XDHSpec; import org.pgpainless.key.util.KeyRingUtils; import org.pgpainless.util.Passphrase; +import javax.annotation.Nonnull; + public class EncryptionOptionsTest { private static PGPSecretKeyRing secretKeys; @@ -149,7 +151,7 @@ public class EncryptionOptionsTest { assertThrows(KeyException.UnacceptableEncryptionKeyException.class, () -> options.addRecipient(publicKeys, new EncryptionOptions.EncryptionKeySelector() { @Override - public List selectEncryptionSubkeys(List encryptionCapableKeys) { + public List selectEncryptionSubkeys(@Nonnull List encryptionCapableKeys) { return Collections.emptyList(); } })); @@ -157,7 +159,7 @@ public class EncryptionOptionsTest { assertThrows(KeyException.UnacceptableEncryptionKeyException.class, () -> options.addRecipient(publicKeys, "test@pgpainless.org", new EncryptionOptions.EncryptionKeySelector() { @Override - public List selectEncryptionSubkeys(List encryptionCapableKeys) { + public List selectEncryptionSubkeys(@Nonnull List encryptionCapableKeys) { return Collections.emptyList(); } })); From 1d26751b45b00519894f91f80dbf5d3bc52b4d04 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 3 May 2023 15:59:21 +0200 Subject: [PATCH 288/763] Remove unused KeyRingEditorTest --- .../key/modification/KeyRingEditorTest.java | 19 ------------------- 1 file changed, 19 deletions(-) delete mode 100644 pgpainless-core/src/test/java/org/pgpainless/key/modification/KeyRingEditorTest.java diff --git a/pgpainless-core/src/test/java/org/pgpainless/key/modification/KeyRingEditorTest.java b/pgpainless-core/src/test/java/org/pgpainless/key/modification/KeyRingEditorTest.java deleted file mode 100644 index 68774cfc..00000000 --- a/pgpainless-core/src/test/java/org/pgpainless/key/modification/KeyRingEditorTest.java +++ /dev/null @@ -1,19 +0,0 @@ -// SPDX-FileCopyrightText: 2020 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.key.modification; - -import static org.junit.jupiter.api.Assertions.assertThrows; - -import org.junit.jupiter.api.Test; -import org.pgpainless.key.modification.secretkeyring.SecretKeyRingEditor; - -public class KeyRingEditorTest { - - @Test - public void testConstructorThrowsNpeForNull() { - assertThrows(NullPointerException.class, - () -> new SecretKeyRingEditor(null)); - } -} From 3b8a1b47d7e2cd24d63a562cd246254159b645b5 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 3 May 2023 16:03:12 +0200 Subject: [PATCH 289/763] Add javadoc p-tags --- .../src/main/java/org/pgpainless/PGPainless.java | 6 +++--- .../encryption_signing/SigningOptions.java | 12 ++++++------ .../java/org/pgpainless/key/info/KeyAccessor.java | 2 +- .../java/org/pgpainless/key/info/KeyRingInfo.java | 8 ++++---- 4 files changed, 14 insertions(+), 14 deletions(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/PGPainless.java b/pgpainless-core/src/main/java/org/pgpainless/PGPainless.java index 6da77c80..16928210 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/PGPainless.java +++ b/pgpainless-core/src/main/java/org/pgpainless/PGPainless.java @@ -152,7 +152,7 @@ public final class PGPainless { /** * Make changes to a secret key. * This method can be used to change key expiration dates and passphrases, or add/revoke subkeys. - * + *

      * After making the desired changes in the builder, the modified key ring can be extracted using {@link SecretKeyRingEditorInterface#done()}. * * @param secretKeys secret key ring @@ -165,7 +165,7 @@ public final class PGPainless { /** * Make changes to a secret key at the given reference time. * This method can be used to change key expiration dates and passphrases, or add/revoke user-ids and subkeys. - * + *

      * After making the desired changes in the builder, the modified key can be extracted using {@link SecretKeyRingEditorInterface#done()}. * * @param secretKeys secret key ring @@ -179,7 +179,7 @@ public final class PGPainless { /** * Quickly access information about a {@link org.bouncycastle.openpgp.PGPPublicKeyRing} / {@link PGPSecretKeyRing}. * This method can be used to determine expiration dates, key flags and other information about a key. - * + *

      * To evaluate a key at a given date (e.g. to determine if the key was allowed to create a certain signature) * use {@link #inspectKeyRing(PGPKeyRing, Date)} instead. * diff --git a/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/SigningOptions.java b/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/SigningOptions.java index 0af07fc9..77f95efb 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/SigningOptions.java +++ b/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/SigningOptions.java @@ -158,7 +158,7 @@ public final class SigningOptions { * Add an inline-signature. * Inline signatures are being embedded into the message itself and can be processed in one pass, thanks to the use * of one-pass-signature packets. - * + *

      * This method uses the passed in user-id to select user-specific hash algorithms. * * @param secretKeyDecryptor decryptor to unlock the signing secret key @@ -182,7 +182,7 @@ public final class SigningOptions { * Add an inline-signature. * Inline signatures are being embedded into the message itself and can be processed in one pass, thanks to the use * of one-pass-signature packets. - * + *

      * This method uses the passed in user-id to select user-specific hash algorithms. * * @param secretKeyDecryptor decryptor to unlock the signing secret key @@ -295,7 +295,7 @@ public final class SigningOptions { * Detached signatures are not being added into the PGP message itself. * Instead, they can be distributed separately to the message. * Detached signatures are useful if the data that is being signed shall not be modified (e.g. when signing a file). - * + *

      * This method uses the passed in user-id to select user-specific hash algorithms. * * @param secretKeyDecryptor decryptor to unlock the secret signing key @@ -320,7 +320,7 @@ public final class SigningOptions { * Detached signatures are not being added into the PGP message itself. * Instead, they can be distributed separately to the message. * Detached signatures are useful if the data that is being signed shall not be modified (e.g. when signing a file). - * + *

      * This method uses the passed in user-id to select user-specific hash algorithms. * * @param secretKeyDecryptor decryptor to unlock the secret signing key @@ -406,7 +406,7 @@ public final class SigningOptions { /** * Negotiate, which hash algorithm to use. - * + *

      * This method gives the highest priority to the algorithm override, which can be set via {@link #overrideHashAlgorithm(HashAlgorithm)}. * After that, the signing keys hash algorithm preferences are iterated to find the first acceptable algorithm. * Lastly, should no acceptable algorithm be found, the {@link Policy Policies} default signature hash algorithm is @@ -451,7 +451,7 @@ public final class SigningOptions { /** * Override hash algorithm negotiation by dictating which hash algorithm needs to be used. * If no override has been set, an accetable algorithm will be negotiated instead. - * + *

      * Note: To override the hash algorithm for signing, call this method *before* calling * {@link #addInlineSignature(SecretKeyRingProtector, PGPSecretKeyRing, DocumentSignatureType)} or * {@link #addDetachedSignature(SecretKeyRingProtector, PGPSecretKeyRing, DocumentSignatureType)}. diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/info/KeyAccessor.java b/pgpainless-core/src/main/java/org/pgpainless/key/info/KeyAccessor.java index 5fa71d46..48102931 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/key/info/KeyAccessor.java +++ b/pgpainless-core/src/main/java/org/pgpainless/key/info/KeyAccessor.java @@ -28,7 +28,7 @@ public abstract class KeyAccessor { /** * Depending on the way we address the key (key-id or user-id), return the respective {@link PGPSignature} * which contains the algorithm preferences we are going to use. - * + *

      * If we address a key via its user-id, we want to rely on the algorithm preferences in the user-id certification, * while we would instead rely on those in the direct-key signature if we'd address the key by key-id. * diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/info/KeyRingInfo.java b/pgpainless-core/src/main/java/org/pgpainless/key/info/KeyRingInfo.java index 75d9e16c..45de52f3 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/key/info/KeyRingInfo.java +++ b/pgpainless-core/src/main/java/org/pgpainless/key/info/KeyRingInfo.java @@ -292,7 +292,7 @@ public class KeyRingInfo { /** * Return the current primary user-id of the key ring. - * + *

      * Note: If no user-id is marked as primary key using a {@link PrimaryUserID} packet, * this method returns the first user-id on the key, otherwise null. * @@ -472,7 +472,7 @@ public class KeyRingInfo { /** * Return the latest direct-key self signature. - * + *

      * Note: This signature might be expired (check with {@link SignatureUtils#isSignatureExpired(PGPSignature)}). * * @return latest direct key self-signature or null @@ -782,7 +782,7 @@ public class KeyRingInfo { * Return the latest date on which the key ring is still usable for the given key flag. * If only a subkey is carrying the required flag and the primary key expires earlier than the subkey, * the expiry date of the primary key is returned. - * + *

      * This method might return null, if the primary key and a subkey with the required flag does not expire. * @param use key flag representing the use case, e.g. {@link KeyFlag#SIGN_DATA} or * {@link KeyFlag#ENCRYPT_COMMS}/{@link KeyFlag#ENCRYPT_STORAGE}. @@ -1133,7 +1133,7 @@ public class KeyRingInfo { /** * Returns true, if this {@link KeyRingInfo} is based on a {@link PGPSecretKeyRing}, which has a valid signing key * which is ready to be used (i.e. secret key is present and is not on a smart-card). - * + *

      * If you just want to check, whether a key / certificate has signing capable subkeys, * use {@link #isSigningCapable()} instead. * From 953206b4ed39b692a4c7caa9d9b586382bf81729 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 3 May 2023 16:03:50 +0200 Subject: [PATCH 290/763] Make more of the API null-safe by using @Nonnull/@Nullable --- .../main/java/org/pgpainless/PGPainless.java | 26 ++- .../OpenPgpMessageInputStream.java | 2 +- .../BcPGPHashContextContentSignerBuilder.java | 9 +- .../encryption_signing/SigningOptions.java | 114 ++++++----- .../org/pgpainless/key/info/KeyAccessor.java | 39 +++- .../org/pgpainless/key/info/KeyRingInfo.java | 183 ++++++++++++------ .../secretkeyring/SecretKeyRingEditor.java | 10 +- .../algorithm/RevocationStateTest.java | 5 - .../key/info/UserIdRevocationTest.java | 1 + .../SignatureSubpacketsUtilTest.java | 3 + ...artyCertificationSignatureBuilderTest.java | 6 +- .../subpackets/SignatureSubpacketsTest.java | 2 +- 12 files changed, 263 insertions(+), 137 deletions(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/PGPainless.java b/pgpainless-core/src/main/java/org/pgpainless/PGPainless.java index 16928210..3da54177 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/PGPainless.java +++ b/pgpainless-core/src/main/java/org/pgpainless/PGPainless.java @@ -40,6 +40,7 @@ public final class PGPainless { * Generate a fresh OpenPGP key ring from predefined templates. * @return templates */ + @Nonnull public static KeyRingTemplates generateKeyRing() { return new KeyRingTemplates(); } @@ -49,6 +50,7 @@ public final class PGPainless { * * @return builder */ + @Nonnull public static KeyRingBuilder buildKeyRing() { return new KeyRingBuilder(); } @@ -57,6 +59,7 @@ public final class PGPainless { * Read an existing OpenPGP key ring. * @return builder */ + @Nonnull public static KeyRingReader readKeyRing() { return new KeyRingReader(); } @@ -67,6 +70,7 @@ public final class PGPainless { * @param secretKey secret key * @return public key certificate */ + @Nonnull public static PGPPublicKeyRing extractCertificate(@Nonnull PGPSecretKeyRing secretKey) { return KeyRingUtils.publicKeyRingFrom(secretKey); } @@ -79,6 +83,7 @@ public final class PGPainless { * @return merged certificate * @throws PGPException in case of an error */ + @Nonnull public static PGPPublicKeyRing mergeCertificate( @Nonnull PGPPublicKeyRing originalCopy, @Nonnull PGPPublicKeyRing updatedCopy) @@ -94,6 +99,7 @@ public final class PGPainless { * * @throws IOException in case of an error in the {@link ArmoredOutputStream} */ + @Nonnull public static String asciiArmor(@Nonnull PGPKeyRing key) throws IOException { if (key instanceof PGPSecretKeyRing) { @@ -111,6 +117,7 @@ public final class PGPainless { * * @throws IOException in case of an error in the {@link ArmoredOutputStream} */ + @Nonnull public static String asciiArmor(@Nonnull PGPSignature signature) throws IOException { return ArmorUtils.toAsciiArmoredString(signature); @@ -136,6 +143,7 @@ public final class PGPainless { * * @return builder */ + @Nonnull public static EncryptionBuilder encryptAndOrSign() { return new EncryptionBuilder(); } @@ -145,6 +153,7 @@ public final class PGPainless { * * @return builder */ + @Nonnull public static DecryptionBuilder decryptAndOrVerify() { return new DecryptionBuilder(); } @@ -158,8 +167,9 @@ public final class PGPainless { * @param secretKeys secret key ring * @return builder */ - public static SecretKeyRingEditorInterface modifyKeyRing(PGPSecretKeyRing secretKeys) { - return modifyKeyRing(secretKeys, null); + @Nonnull + public static SecretKeyRingEditorInterface modifyKeyRing(@Nonnull PGPSecretKeyRing secretKeys) { + return modifyKeyRing(secretKeys, new Date()); } /** @@ -172,7 +182,9 @@ public final class PGPainless { * @param referenceTime reference time used as signature creation date * @return builder */ - public static SecretKeyRingEditorInterface modifyKeyRing(PGPSecretKeyRing secretKeys, Date referenceTime) { + @Nonnull + public static SecretKeyRingEditorInterface modifyKeyRing(@Nonnull PGPSecretKeyRing secretKeys, + @Nonnull Date referenceTime) { return new SecretKeyRingEditor(secretKeys, referenceTime); } @@ -186,7 +198,8 @@ public final class PGPainless { * @param keyRing key ring * @return access object */ - public static KeyRingInfo inspectKeyRing(PGPKeyRing keyRing) { + @Nonnull + public static KeyRingInfo inspectKeyRing(@Nonnull PGPKeyRing keyRing) { return new KeyRingInfo(keyRing); } @@ -198,7 +211,8 @@ public final class PGPainless { * @param referenceTime date of inspection * @return access object */ - public static KeyRingInfo inspectKeyRing(PGPKeyRing keyRing, Date referenceTime) { + @Nonnull + public static KeyRingInfo inspectKeyRing(@Nonnull PGPKeyRing keyRing, @Nonnull Date referenceTime) { return new KeyRingInfo(keyRing, referenceTime); } @@ -207,6 +221,7 @@ public final class PGPainless { * * @return policy */ + @Nonnull public static Policy getPolicy() { return Policy.getInstance(); } @@ -216,6 +231,7 @@ public final class PGPainless { * * @return builder */ + @Nonnull public static CertifyCertificate certify() { return new CertifyCertificate(); } diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java index d51a6cf7..04d823d1 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java @@ -359,7 +359,7 @@ public class OpenPgpMessageInputStream extends DecryptionStream { PGPCompressedData compressedData = packetInputStream.readCompressedData(); // Extract Metadata MessageMetadata.CompressedData compressionLayer = new MessageMetadata.CompressedData( - CompressionAlgorithm.fromId(compressedData.getAlgorithm()), + CompressionAlgorithm.requireFromId(compressedData.getAlgorithm()), metadata.depth + 1); LOGGER.debug("Compressed Data Packet (" + compressionLayer.algorithm + ") at depth " + metadata.depth + " encountered"); diff --git a/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/BcPGPHashContextContentSignerBuilder.java b/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/BcPGPHashContextContentSignerBuilder.java index df6f6ca3..5cdf9e36 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/BcPGPHashContextContentSignerBuilder.java +++ b/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/BcPGPHashContextContentSignerBuilder.java @@ -44,10 +44,15 @@ class BcPGPHashContextContentSignerBuilder extends PGPHashContextContentSignerBu BcPGPHashContextContentSignerBuilder(MessageDigest messageDigest) { this.messageDigest = messageDigest; - this.hashAlgorithm = HashAlgorithm.fromName(messageDigest.getAlgorithm()); + this.hashAlgorithm = requireFromName(messageDigest.getAlgorithm()); + } + + private static HashAlgorithm requireFromName(String digestName) { + HashAlgorithm hashAlgorithm = HashAlgorithm.fromName(digestName); if (hashAlgorithm == null) { - throw new IllegalArgumentException("Cannot recognize OpenPGP Hash Algorithm: " + messageDigest.getAlgorithm()); + throw new IllegalArgumentException("Cannot recognize OpenPGP Hash Algorithm: " + digestName); } + return hashAlgorithm; } @Override diff --git a/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/SigningOptions.java b/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/SigningOptions.java index 77f95efb..a899bd12 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/SigningOptions.java +++ b/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/SigningOptions.java @@ -10,6 +10,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Set; +import javax.annotation.Nonnull; import javax.annotation.Nullable; import org.bouncycastle.openpgp.PGPException; @@ -46,7 +47,9 @@ public final class SigningOptions { private final boolean detached; private final HashAlgorithm hashAlgorithm; - private SigningMethod(PGPSignatureGenerator signatureGenerator, boolean detached, HashAlgorithm hashAlgorithm) { + private SigningMethod(@Nonnull PGPSignatureGenerator signatureGenerator, + boolean detached, + @Nonnull HashAlgorithm hashAlgorithm) { this.signatureGenerator = signatureGenerator; this.detached = detached; this.hashAlgorithm = hashAlgorithm; @@ -60,7 +63,8 @@ public final class SigningOptions { * @param hashAlgorithm hash algorithm used to generate the signature * @return inline signing method */ - public static SigningMethod inlineSignature(PGPSignatureGenerator signatureGenerator, HashAlgorithm hashAlgorithm) { + public static SigningMethod inlineSignature(@Nonnull PGPSignatureGenerator signatureGenerator, + @Nonnull HashAlgorithm hashAlgorithm) { return new SigningMethod(signatureGenerator, false, hashAlgorithm); } @@ -73,7 +77,8 @@ public final class SigningOptions { * @param hashAlgorithm hash algorithm used to generate the signature * @return detached signing method */ - public static SigningMethod detachedSignature(PGPSignatureGenerator signatureGenerator, HashAlgorithm hashAlgorithm) { + public static SigningMethod detachedSignature(@Nonnull PGPSignatureGenerator signatureGenerator, + @Nonnull HashAlgorithm hashAlgorithm) { return new SigningMethod(signatureGenerator, true, hashAlgorithm); } @@ -93,6 +98,7 @@ public final class SigningOptions { private final Map signingMethods = new HashMap<>(); private HashAlgorithm hashAlgorithmOverride; + @Nonnull public static SigningOptions get() { return new SigningOptions(); } @@ -107,8 +113,9 @@ public final class SigningOptions { * @throws KeyException if something is wrong with the key * @throws PGPException if the key cannot be unlocked or a signing method cannot be created */ - public SigningOptions addSignature(SecretKeyRingProtector signingKeyProtector, - PGPSecretKeyRing signingKey) + @Nonnull + public SigningOptions addSignature(@Nonnull SecretKeyRingProtector signingKeyProtector, + @Nonnull PGPSecretKeyRing signingKey) throws PGPException { return addInlineSignature(signingKeyProtector, signingKey, DocumentSignatureType.BINARY_DOCUMENT); } @@ -124,9 +131,10 @@ public final class SigningOptions { * @throws KeyException if something is wrong with any of the keys * @throws PGPException if any of the keys cannot be unlocked or a signing method cannot be created */ - public SigningOptions addInlineSignatures(SecretKeyRingProtector secrectKeyDecryptor, - Iterable signingKeys, - DocumentSignatureType signatureType) + @Nonnull + public SigningOptions addInlineSignatures(@Nonnull SecretKeyRingProtector secrectKeyDecryptor, + @Nonnull Iterable signingKeys, + @Nonnull DocumentSignatureType signatureType) throws KeyException, PGPException { for (PGPSecretKeyRing signingKey : signingKeys) { addInlineSignature(secrectKeyDecryptor, signingKey, signatureType); @@ -147,9 +155,10 @@ public final class SigningOptions { * @throws KeyException if something is wrong with the key * @throws PGPException if the key cannot be unlocked or the signing method cannot be created */ - public SigningOptions addInlineSignature(SecretKeyRingProtector secretKeyDecryptor, - PGPSecretKeyRing secretKey, - DocumentSignatureType signatureType) + @Nonnull + public SigningOptions addInlineSignature(@Nonnull SecretKeyRingProtector secretKeyDecryptor, + @Nonnull PGPSecretKeyRing secretKey, + @Nonnull DocumentSignatureType signatureType) throws KeyException, PGPException { return addInlineSignature(secretKeyDecryptor, secretKey, null, signatureType); } @@ -170,10 +179,11 @@ public final class SigningOptions { * @throws KeyException if something is wrong with the key * @throws PGPException if the key cannot be unlocked or the signing method cannot be created */ - public SigningOptions addInlineSignature(SecretKeyRingProtector secretKeyDecryptor, - PGPSecretKeyRing secretKey, - String userId, - DocumentSignatureType signatureType) + @Nonnull + public SigningOptions addInlineSignature(@Nonnull SecretKeyRingProtector secretKeyDecryptor, + @Nonnull PGPSecretKeyRing secretKey, + @Nullable CharSequence userId, + @Nonnull DocumentSignatureType signatureType) throws KeyException, PGPException { return addInlineSignature(secretKeyDecryptor, secretKey, userId, signatureType, null); } @@ -195,17 +205,18 @@ public final class SigningOptions { * @throws KeyException if the key is invalid * @throws PGPException if the key cannot be unlocked or the signing method cannot be created */ - public SigningOptions addInlineSignature(SecretKeyRingProtector secretKeyDecryptor, - PGPSecretKeyRing secretKey, - String userId, - DocumentSignatureType signatureType, + @Nonnull + public SigningOptions addInlineSignature(@Nonnull SecretKeyRingProtector secretKeyDecryptor, + @Nonnull PGPSecretKeyRing secretKey, + @Nullable CharSequence userId, + @Nonnull DocumentSignatureType signatureType, @Nullable BaseSignatureSubpackets.Callback subpacketsCallback) throws KeyException, PGPException { KeyRingInfo keyRingInfo = new KeyRingInfo(secretKey, new Date()); if (userId != null && !keyRingInfo.isUserIdValid(userId)) { throw new KeyException.UnboundUserIdException( OpenPgpFingerprint.of(secretKey), - userId, + userId.toString(), keyRingInfo.getLatestUserIdCertification(userId), keyRingInfo.getUserIdRevocation(userId) ); @@ -242,9 +253,10 @@ public final class SigningOptions { * @throws KeyException if something is wrong with any of the keys * @throws PGPException if any of the keys cannot be validated or unlocked, or if any signing method cannot be created */ - public SigningOptions addDetachedSignatures(SecretKeyRingProtector secretKeyDecryptor, - Iterable signingKeys, - DocumentSignatureType signatureType) + @Nonnull + public SigningOptions addDetachedSignatures(@Nonnull SecretKeyRingProtector secretKeyDecryptor, + @Nonnull Iterable signingKeys, + @Nonnull DocumentSignatureType signatureType) throws PGPException { for (PGPSecretKeyRing signingKey : signingKeys) { addDetachedSignature(secretKeyDecryptor, signingKey, signatureType); @@ -263,8 +275,9 @@ public final class SigningOptions { * @throws KeyException if something is wrong with the key * @throws PGPException if the key cannot be validated or unlocked, or if no signature method can be created */ - public SigningOptions addDetachedSignature(SecretKeyRingProtector secretKeyDecryptor, - PGPSecretKeyRing signingKey) + @Nonnull + public SigningOptions addDetachedSignature(@Nonnull SecretKeyRingProtector secretKeyDecryptor, + @Nonnull PGPSecretKeyRing signingKey) throws PGPException { return addDetachedSignature(secretKeyDecryptor, signingKey, DocumentSignatureType.BINARY_DOCUMENT); } @@ -283,9 +296,10 @@ public final class SigningOptions { * @throws KeyException if something is wrong with the key * @throws PGPException if the key cannot be validated or unlocked, or if no signature method can be created */ - public SigningOptions addDetachedSignature(SecretKeyRingProtector secretKeyDecryptor, - PGPSecretKeyRing secretKey, - DocumentSignatureType signatureType) + @Nonnull + public SigningOptions addDetachedSignature(@Nonnull SecretKeyRingProtector secretKeyDecryptor, + @Nonnull PGPSecretKeyRing secretKey, + @Nonnull DocumentSignatureType signatureType) throws PGPException { return addDetachedSignature(secretKeyDecryptor, secretKey, null, signatureType); } @@ -307,10 +321,11 @@ public final class SigningOptions { * @throws KeyException if something is wrong with the key * @throws PGPException if the key cannot be validated or unlocked, or if no signature method can be created */ - public SigningOptions addDetachedSignature(SecretKeyRingProtector secretKeyDecryptor, - PGPSecretKeyRing secretKey, - String userId, - DocumentSignatureType signatureType) + @Nonnull + public SigningOptions addDetachedSignature(@Nonnull SecretKeyRingProtector secretKeyDecryptor, + @Nonnull PGPSecretKeyRing secretKey, + @Nullable CharSequence userId, + @Nonnull DocumentSignatureType signatureType) throws PGPException { return addDetachedSignature(secretKeyDecryptor, secretKey, userId, signatureType, null); } @@ -333,17 +348,18 @@ public final class SigningOptions { * @throws KeyException if something is wrong with the key * @throws PGPException if the key cannot be validated or unlocked, or if no signature method can be created */ - public SigningOptions addDetachedSignature(SecretKeyRingProtector secretKeyDecryptor, - PGPSecretKeyRing secretKey, - String userId, - DocumentSignatureType signatureType, + @Nonnull + public SigningOptions addDetachedSignature(@Nonnull SecretKeyRingProtector secretKeyDecryptor, + @Nonnull PGPSecretKeyRing secretKey, + @Nullable CharSequence userId, + @Nonnull DocumentSignatureType signatureType, @Nullable BaseSignatureSubpackets.Callback subpacketCallback) throws PGPException { KeyRingInfo keyRingInfo = new KeyRingInfo(secretKey, new Date()); if (userId != null && !keyRingInfo.isUserIdValid(userId)) { throw new KeyException.UnboundUserIdException( OpenPgpFingerprint.of(secretKey), - userId, + userId.toString(), keyRingInfo.getLatestUserIdCertification(userId), keyRingInfo.getUserIdRevocation(userId) ); @@ -369,11 +385,11 @@ public final class SigningOptions { return this; } - private void addSigningMethod(PGPSecretKeyRing secretKey, - PGPPrivateKey signingSubkey, + private void addSigningMethod(@Nonnull PGPSecretKeyRing secretKey, + @Nonnull PGPPrivateKey signingSubkey, @Nullable BaseSignatureSubpackets.Callback subpacketCallback, - HashAlgorithm hashAlgorithm, - DocumentSignatureType signatureType, + @Nonnull HashAlgorithm hashAlgorithm, + @Nonnull DocumentSignatureType signatureType, boolean detached) throws PGPException { SubkeyIdentifier signingKeyIdentifier = new SubkeyIdentifier(secretKey, signingSubkey.getKeyID()); @@ -416,7 +432,9 @@ public final class SigningOptions { * @param policy policy * @return selected hash algorithm */ - private HashAlgorithm negotiateHashAlgorithm(Set preferences, Policy policy) { + @Nonnull + private HashAlgorithm negotiateHashAlgorithm(@Nonnull Set preferences, + @Nonnull Policy policy) { if (hashAlgorithmOverride != null) { return hashAlgorithmOverride; } @@ -425,9 +443,10 @@ public final class SigningOptions { .negotiateHashAlgorithm(preferences); } - private PGPSignatureGenerator createSignatureGenerator(PGPPrivateKey privateKey, - HashAlgorithm hashAlgorithm, - DocumentSignatureType signatureType) + @Nonnull + private PGPSignatureGenerator createSignatureGenerator(@Nonnull PGPPrivateKey privateKey, + @Nonnull HashAlgorithm hashAlgorithm, + @Nonnull DocumentSignatureType signatureType) throws PGPException { int publicKeyAlgorithm = privateKey.getPublicKeyPacket().getAlgorithm(); PGPContentSignerBuilder signerBuilder = ImplementationFactory.getInstance() @@ -444,6 +463,7 @@ public final class SigningOptions { * * @return signing methods */ + @Nonnull Map getSigningMethods() { return Collections.unmodifiableMap(signingMethods); } @@ -459,7 +479,8 @@ public final class SigningOptions { * @param hashAlgorithmOverride override hash algorithm * @return this */ - public SigningOptions overrideHashAlgorithm(HashAlgorithm hashAlgorithmOverride) { + @Nonnull + public SigningOptions overrideHashAlgorithm(@Nonnull HashAlgorithm hashAlgorithmOverride) { this.hashAlgorithmOverride = hashAlgorithmOverride; return this; } @@ -469,6 +490,7 @@ public final class SigningOptions { * * @return hash algorithm override */ + @Nullable public HashAlgorithm getHashAlgorithmOverride() { return hashAlgorithmOverride; } diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/info/KeyAccessor.java b/pgpainless-core/src/main/java/org/pgpainless/key/info/KeyAccessor.java index 48102931..8ab8a9c4 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/key/info/KeyAccessor.java +++ b/pgpainless-core/src/main/java/org/pgpainless/key/info/KeyAccessor.java @@ -20,7 +20,7 @@ public abstract class KeyAccessor { protected final KeyRingInfo info; protected final SubkeyIdentifier key; - KeyAccessor(KeyRingInfo info, SubkeyIdentifier key) { + KeyAccessor(@Nonnull KeyRingInfo info, @Nonnull SubkeyIdentifier key) { this.info = info; this.key = key; } @@ -34,13 +34,15 @@ public abstract class KeyAccessor { * * @return signature */ - public abstract @Nonnull PGPSignature getSignatureWithPreferences(); + @Nonnull + public abstract PGPSignature getSignatureWithPreferences(); /** * Return preferred symmetric key encryption algorithms. * * @return preferred symmetric algorithms */ + @Nonnull public Set getPreferredSymmetricKeyAlgorithms() { return SignatureSubpacketsUtil.parsePreferredSymmetricKeyAlgorithms(getSignatureWithPreferences()); } @@ -50,6 +52,7 @@ public abstract class KeyAccessor { * * @return preferred hash algorithms */ + @Nonnull public Set getPreferredHashAlgorithms() { return SignatureSubpacketsUtil.parsePreferredHashAlgorithms(getSignatureWithPreferences()); } @@ -59,6 +62,7 @@ public abstract class KeyAccessor { * * @return preferred compression algorithms */ + @Nonnull public Set getPreferredCompressionAlgorithms() { return SignatureSubpacketsUtil.parsePreferredCompressionAlgorithms(getSignatureWithPreferences()); } @@ -78,13 +82,16 @@ public abstract class KeyAccessor { * @param key id of the subkey * @param userId user-id */ - public ViaUserId(KeyRingInfo info, SubkeyIdentifier key, String userId) { + public ViaUserId(@Nonnull KeyRingInfo info, + @Nonnull SubkeyIdentifier key, + @Nonnull String userId) { super(info, key); this.userId = userId; } @Override - public @Nonnull PGPSignature getSignatureWithPreferences() { + @Nonnull + public PGPSignature getSignatureWithPreferences() { PGPSignature signature = info.getLatestUserIdCertification(userId); if (signature != null) { return signature; @@ -104,19 +111,26 @@ public abstract class KeyAccessor { * @param info info about the key at a given date * @param key key-id */ - public ViaKeyId(KeyRingInfo info, SubkeyIdentifier key) { + public ViaKeyId(@Nonnull KeyRingInfo info, + @Nonnull SubkeyIdentifier key) { super(info, key); } @Override - public @Nonnull PGPSignature getSignatureWithPreferences() { + @Nonnull + public PGPSignature getSignatureWithPreferences() { String primaryUserId = info.getPrimaryUserId(); // If the key is located by Key ID, the algorithm of the primary User ID of the key provides the // preferred symmetric algorithm. - PGPSignature signature = info.getLatestUserIdCertification(primaryUserId); + PGPSignature signature = null; + if (primaryUserId != null) { + signature = info.getLatestUserIdCertification(primaryUserId); + } + if (signature == null) { signature = info.getLatestDirectKeySelfSignature(); } + if (signature == null) { throw new IllegalStateException("No valid signature found."); } @@ -126,22 +140,27 @@ public abstract class KeyAccessor { public static class SubKey extends KeyAccessor { - public SubKey(KeyRingInfo info, SubkeyIdentifier key) { + public SubKey(@Nonnull KeyRingInfo info, + @Nonnull SubkeyIdentifier key) { super(info, key); } @Override - public @Nonnull PGPSignature getSignatureWithPreferences() { + @Nonnull + public PGPSignature getSignatureWithPreferences() { PGPSignature signature; if (key.getPrimaryKeyId() == key.getSubkeyId()) { signature = info.getLatestDirectKeySelfSignature(); - if (signature == null) { + if (signature == null && info.getPrimaryUserId() != null) { signature = info.getLatestUserIdCertification(info.getPrimaryUserId()); } } else { signature = info.getCurrentSubkeyBindingSignature(key.getSubkeyId()); } + if (signature == null) { + throw new IllegalStateException("No valid signature found."); + } return signature; } } diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/info/KeyRingInfo.java b/pgpainless-core/src/main/java/org/pgpainless/key/info/KeyRingInfo.java index 45de52f3..1c20a06c 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/key/info/KeyRingInfo.java +++ b/pgpainless-core/src/main/java/org/pgpainless/key/info/KeyRingInfo.java @@ -74,7 +74,9 @@ public class KeyRingInfo { * @param signature signature * @return info of key ring at signature creation time */ - public static KeyRingInfo evaluateForSignature(PGPKeyRing keyRing, PGPSignature signature) { + @Nonnull + public static KeyRingInfo evaluateForSignature(@Nonnull PGPKeyRing keyRing, + @Nonnull PGPSignature signature) { return new KeyRingInfo(keyRing, signature.getCreationTime()); } @@ -83,7 +85,7 @@ public class KeyRingInfo { * * @param keys key ring */ - public KeyRingInfo(PGPKeyRing keys) { + public KeyRingInfo(@Nonnull PGPKeyRing keys) { this(keys, new Date()); } @@ -93,7 +95,8 @@ public class KeyRingInfo { * @param keys key ring * @param referenceDate date of validation */ - public KeyRingInfo(PGPKeyRing keys, Date referenceDate) { + public KeyRingInfo(@Nonnull PGPKeyRing keys, + @Nonnull Date referenceDate) { this(keys, PGPainless.getPolicy(), referenceDate); } @@ -104,14 +107,17 @@ public class KeyRingInfo { * @param policy policy * @param referenceDate validation date */ - public KeyRingInfo(PGPKeyRing keys, Policy policy, Date referenceDate) { - this.referenceDate = referenceDate != null ? referenceDate : new Date(); + public KeyRingInfo(@Nonnull PGPKeyRing keys, + @Nonnull Policy policy, + @Nonnull Date referenceDate) { + this.referenceDate = referenceDate; this.keys = keys; this.signatures = new Signatures(keys, this.referenceDate, policy); this.primaryUserId = findPrimaryUserId(); this.revocationState = findRevocationState(); } + @Nonnull private RevocationState findRevocationState() { PGPSignature revocation = signatures.primaryKeyRevocation; if (revocation != null) { @@ -126,6 +132,7 @@ public class KeyRingInfo { * * @return public key */ + @Nonnull public PGPPublicKey getPublicKey() { return keys.getPublicKey(); } @@ -136,7 +143,8 @@ public class KeyRingInfo { * @param fingerprint fingerprint * @return public key or null */ - public @Nullable PGPPublicKey getPublicKey(OpenPgpFingerprint fingerprint) { + @Nullable + public PGPPublicKey getPublicKey(@Nonnull OpenPgpFingerprint fingerprint) { return getPublicKey(fingerprint.getKeyId()); } @@ -146,7 +154,8 @@ public class KeyRingInfo { * @param keyId key id * @return public key or null */ - public @Nullable PGPPublicKey getPublicKey(long keyId) { + @Nullable + public PGPPublicKey getPublicKey(long keyId) { return getPublicKey(keys, keyId); } @@ -157,7 +166,8 @@ public class KeyRingInfo { * @param keyId key id * @return public key or null */ - public static @Nullable PGPPublicKey getPublicKey(PGPKeyRing keyRing, long keyId) { + @Nullable + public static PGPPublicKey getPublicKey(@Nonnull PGPKeyRing keyRing, long keyId) { return keyRing.getPublicKey(keyId); } @@ -210,6 +220,7 @@ public class KeyRingInfo { * * @return list of public keys */ + @Nonnull public List getPublicKeys() { Iterator iterator = keys.getPublicKeys(); List list = iteratorToList(iterator); @@ -221,7 +232,8 @@ public class KeyRingInfo { * * @return primary secret key or null if the key ring is public */ - public @Nullable PGPSecretKey getSecretKey() { + @Nullable + public PGPSecretKey getSecretKey() { if (keys instanceof PGPSecretKeyRing) { PGPSecretKeyRing secretKeys = (PGPSecretKeyRing) keys; return secretKeys.getSecretKey(); @@ -235,7 +247,8 @@ public class KeyRingInfo { * @param fingerprint fingerprint * @return secret key or null */ - public @Nullable PGPSecretKey getSecretKey(OpenPgpFingerprint fingerprint) { + @Nullable + public PGPSecretKey getSecretKey(@Nonnull OpenPgpFingerprint fingerprint) { return getSecretKey(fingerprint.getKeyId()); } @@ -245,7 +258,8 @@ public class KeyRingInfo { * @param keyId key id * @return secret key or null */ - public @Nullable PGPSecretKey getSecretKey(long keyId) { + @Nullable + public PGPSecretKey getSecretKey(long keyId) { if (keys instanceof PGPSecretKeyRing) { return ((PGPSecretKeyRing) keys).getSecretKey(keyId); } @@ -259,6 +273,7 @@ public class KeyRingInfo { * * @return list of secret keys */ + @Nonnull public List getSecretKeys() { if (keys instanceof PGPSecretKeyRing) { PGPSecretKeyRing secretKeys = (PGPSecretKeyRing) keys; @@ -282,11 +297,13 @@ public class KeyRingInfo { * * @return fingerprint */ + @Nonnull public OpenPgpFingerprint getFingerprint() { return OpenPgpFingerprint.of(getPublicKey()); } - public @Nullable String getPrimaryUserId() { + @Nullable + public String getPrimaryUserId() { return primaryUserId; } @@ -298,6 +315,7 @@ public class KeyRingInfo { * * @return primary user-id or null */ + @Nullable private String findPrimaryUserId() { String primaryUserId = null; Date currentModificationDate = null; @@ -342,10 +360,10 @@ public class KeyRingInfo { * * @return list of user-ids */ + @Nonnull public List getUserIds() { Iterator iterator = getPublicKey().getUserIDs(); - List userIds = iteratorToList(iterator); - return userIds; + return iteratorToList(iterator); } /** @@ -353,6 +371,7 @@ public class KeyRingInfo { * * @return valid user-ids */ + @Nonnull public List getValidUserIds() { List valid = new ArrayList<>(); List userIds = getUserIds(); @@ -369,6 +388,7 @@ public class KeyRingInfo { * * @return bound user-ids */ + @Nonnull public List getValidAndExpiredUserIds() { List probablyExpired = new ArrayList<>(); List userIds = getUserIds(); @@ -407,21 +427,25 @@ public class KeyRingInfo { * @param userId user-id * @return true if user-id is valid */ - public boolean isUserIdValid(String userId) { + public boolean isUserIdValid(@Nonnull CharSequence userId) { + if (primaryUserId == null) { + // No primary userID? No userID at all! + return false; + } + if (!userId.equals(primaryUserId)) { if (!isUserIdBound(primaryUserId)) { - // primary user-id not valid + // primary user-id not valid? UserID not valid! return false; } } return isUserIdBound(userId); } - - private boolean isUserIdBound(String userId) { - - PGPSignature certification = signatures.userIdCertifications.get(userId); - PGPSignature revocation = signatures.userIdRevocations.get(userId); + private boolean isUserIdBound(@Nonnull CharSequence userId) { + String userIdString = userId.toString(); + PGPSignature certification = signatures.userIdCertifications.get(userIdString); + PGPSignature revocation = signatures.userIdRevocations.get(userIdString); if (certification == null) { return false; @@ -453,6 +477,7 @@ public class KeyRingInfo { * * @return email addresses */ + @Nonnull public List getEmailAddresses() { List userIds = getUserIds(); List emails = new ArrayList<>(); @@ -477,7 +502,8 @@ public class KeyRingInfo { * * @return latest direct key self-signature or null */ - public @Nullable PGPSignature getLatestDirectKeySelfSignature() { + @Nullable + public PGPSignature getLatestDirectKeySelfSignature() { return signatures.primaryKeySelfSignature; } @@ -486,7 +512,8 @@ public class KeyRingInfo { * * @return revocation or null */ - public @Nullable PGPSignature getRevocationSelfSignature() { + @Nullable + public PGPSignature getRevocationSelfSignature() { return signatures.primaryKeyRevocation; } @@ -496,8 +523,9 @@ public class KeyRingInfo { * @param userId user-id * @return certification signature or null */ - public @Nullable PGPSignature getLatestUserIdCertification(String userId) { - return signatures.userIdCertifications.get(userId); + @Nullable + public PGPSignature getLatestUserIdCertification(@Nonnull CharSequence userId) { + return signatures.userIdCertifications.get(userId.toString()); } /** @@ -506,8 +534,9 @@ public class KeyRingInfo { * @param userId user-id * @return revocation or null */ - public @Nullable PGPSignature getUserIdRevocation(String userId) { - return signatures.userIdRevocations.get(userId); + @Nullable + public PGPSignature getUserIdRevocation(@Nonnull CharSequence userId) { + return signatures.userIdRevocations.get(userId.toString()); } /** @@ -516,7 +545,8 @@ public class KeyRingInfo { * @param keyId subkey id * @return subkey binding signature or null */ - public @Nullable PGPSignature getCurrentSubkeyBindingSignature(long keyId) { + @Nullable + public PGPSignature getCurrentSubkeyBindingSignature(long keyId) { return signatures.subkeyBindings.get(keyId); } @@ -526,7 +556,8 @@ public class KeyRingInfo { * @param keyId subkey id * @return subkey binding revocation or null */ - public @Nullable PGPSignature getSubkeyRevocationSignature(long keyId) { + @Nullable + public PGPSignature getSubkeyRevocationSignature(long keyId) { return signatures.subkeyRevocations.get(keyId); } @@ -535,7 +566,8 @@ public class KeyRingInfo { * @param keyId key-id * @return list of key flags */ - public @Nonnull List getKeyFlagsOf(long keyId) { + @Nonnull + public List getKeyFlagsOf(long keyId) { // key is primary key if (getPublicKey().getKeyID() == keyId) { @@ -575,7 +607,8 @@ public class KeyRingInfo { * @param userId user-id * @return key flags */ - public @Nonnull List getKeyFlagsOf(String userId) { + @Nonnull + public List getKeyFlagsOf(String userId) { if (!isUserIdValid(userId)) { return Collections.emptyList(); } @@ -607,6 +640,7 @@ public class KeyRingInfo { * * @return creation date */ + @Nonnull public Date getCreationDate() { return getPublicKey().getCreationTime(); } @@ -617,7 +651,8 @@ public class KeyRingInfo { * * @return last modification date. */ - public @Nonnull Date getLastModified() { + @Nonnull + public Date getLastModified() { PGPSignature mostRecent = getMostRecentSignature(); if (mostRecent == null) { // No sigs found. Return public key creation date instead. @@ -631,7 +666,8 @@ public class KeyRingInfo { * * @return latest key creation time */ - public @Nonnull Date getLatestKeyCreationDate() { + @Nonnull + public Date getLatestKeyCreationDate() { Date latestCreation = null; for (PGPPublicKey key : getPublicKeys()) { if (!isKeyValidlyBound(key.getKeyID())) { @@ -648,7 +684,8 @@ public class KeyRingInfo { return latestCreation; } - private @Nullable PGPSignature getMostRecentSignature() { + @Nullable + private PGPSignature getMostRecentSignature() { Set allSignatures = new HashSet<>(); PGPSignature mostRecentSelfSignature = getLatestDirectKeySelfSignature(); PGPSignature revocationSelfSignature = getRevocationSelfSignature(); @@ -668,6 +705,7 @@ public class KeyRingInfo { return mostRecent; } + @Nonnull public RevocationState getRevocationState() { return revocationState; } @@ -677,7 +715,8 @@ public class KeyRingInfo { * * @return revocation date or null */ - public @Nullable Date getRevocationDate() { + @Nullable + public Date getRevocationDate() { return getRevocationState().isSoftRevocation() ? getRevocationState().getDate() : null; } @@ -686,7 +725,8 @@ public class KeyRingInfo { * * @return expiration date */ - public @Nullable Date getPrimaryKeyExpirationDate() { + @Nullable + public Date getPrimaryKeyExpirationDate() { PGPSignature directKeySig = getLatestDirectKeySelfSignature(); Date directKeyExpirationDate = null; if (directKeySig != null) { @@ -722,6 +762,7 @@ public class KeyRingInfo { return userIdExpirationDate; } + @Nullable public String getPossiblyExpiredPrimaryUserId() { String validPrimaryUserId = getPrimaryUserId(); if (validPrimaryUserId != null) { @@ -760,7 +801,8 @@ public class KeyRingInfo { * @param fingerprint subkey fingerprint * @return expiration date or null */ - public @Nullable Date getSubkeyExpirationDate(OpenPgpFingerprint fingerprint) { + @Nullable + public Date getSubkeyExpirationDate(OpenPgpFingerprint fingerprint) { if (getPublicKey().getKeyID() == fingerprint.getKeyId()) { return getPrimaryKeyExpirationDate(); } @@ -788,6 +830,7 @@ public class KeyRingInfo { * {@link KeyFlag#ENCRYPT_COMMS}/{@link KeyFlag#ENCRYPT_STORAGE}. * @return latest date on which the key ring can be used for the given use case, or null if it can be used indefinitely. */ + @Nullable public Date getExpirationDateForUse(KeyFlag use) { if (use == KeyFlag.SPLIT || use == KeyFlag.SHARED) { throw new IllegalArgumentException("SPLIT and SHARED are not uses, but properties."); @@ -814,20 +857,18 @@ public class KeyRingInfo { } if (nonExpiringSubkeys.isEmpty()) { - if (latestSubkeyExpirationDate != null) { - if (primaryExpiration == null) { - return latestSubkeyExpirationDate; - } - if (latestSubkeyExpirationDate.before(primaryExpiration)) { - return latestSubkeyExpirationDate; - } + if (primaryExpiration == null) { + return latestSubkeyExpirationDate; + } + if (latestSubkeyExpirationDate.before(primaryExpiration)) { + return latestSubkeyExpirationDate; } } return primaryExpiration; } - public boolean isHardRevoked(String userId) { - PGPSignature revocation = signatures.userIdRevocations.get(userId); + public boolean isHardRevoked(@Nonnull CharSequence userId) { + PGPSignature revocation = signatures.userIdRevocations.get(userId.toString()); if (revocation == null) { return false; } @@ -907,7 +948,8 @@ public class KeyRingInfo { * @param purpose purpose (encrypt data at rest / communications) * @return encryption subkeys */ - public @Nonnull List getEncryptionSubkeys(EncryptionPurpose purpose) { + @Nonnull + public List getEncryptionSubkeys(@Nonnull EncryptionPurpose purpose) { Date primaryExpiration = getPrimaryKeyExpirationDate(); if (primaryExpiration != null && primaryExpiration.before(referenceDate)) { LOGGER.debug("Certificate is expired: Primary key is expired on " + DateUtil.formatUTCDate(primaryExpiration)); @@ -966,7 +1008,8 @@ public class KeyRingInfo { * * @return decryption keys */ - public @Nonnull List getDecryptionSubkeys() { + @Nonnull + public List getDecryptionSubkeys() { Iterator subkeys = keys.getPublicKeys(); List decryptionKeys = new ArrayList<>(); @@ -999,7 +1042,8 @@ public class KeyRingInfo { * @param flag flag * @return keys with flag */ - public List getKeysWithKeyFlag(KeyFlag flag) { + @Nonnull + public List getKeysWithKeyFlag(@Nonnull KeyFlag flag) { List keysWithFlag = new ArrayList<>(); for (PGPPublicKey key : getPublicKeys()) { List keyFlags = getKeyFlagsOf(key.getKeyID()); @@ -1021,11 +1065,13 @@ public class KeyRingInfo { * @param purpose encryption purpose * @return encryption subkeys */ - public @Nonnull List getEncryptionSubkeys(String userId, EncryptionPurpose purpose) { + @Nonnull + public List getEncryptionSubkeys(@Nullable CharSequence userId, + @Nonnull EncryptionPurpose purpose) { if (userId != null && !isUserIdValid(userId)) { throw new KeyException.UnboundUserIdException( OpenPgpFingerprint.of(keys), - userId, + userId.toString(), getLatestUserIdCertification(userId), getUserIdRevocation(userId) ); @@ -1039,7 +1085,8 @@ public class KeyRingInfo { * * @return signing keys */ - public @Nonnull List getSigningSubkeys() { + @Nonnull + public List getSigningSubkeys() { Iterator subkeys = keys.getPublicKeys(); List signingKeys = new ArrayList<>(); while (subkeys.hasNext()) { @@ -1057,39 +1104,48 @@ public class KeyRingInfo { return signingKeys; } + @Nonnull public Set getPreferredHashAlgorithms() { return getPreferredHashAlgorithms(getPrimaryUserId()); } - public Set getPreferredHashAlgorithms(String userId) { + @Nonnull + public Set getPreferredHashAlgorithms(@Nullable CharSequence userId) { return getKeyAccessor(userId, getKeyId()).getPreferredHashAlgorithms(); } + @Nonnull public Set getPreferredHashAlgorithms(long keyId) { return new KeyAccessor.SubKey(this, new SubkeyIdentifier(keys, keyId)) .getPreferredHashAlgorithms(); } + @Nonnull public Set getPreferredSymmetricKeyAlgorithms() { return getPreferredSymmetricKeyAlgorithms(getPrimaryUserId()); } - public Set getPreferredSymmetricKeyAlgorithms(String userId) { + @Nonnull + public Set getPreferredSymmetricKeyAlgorithms(@Nullable CharSequence userId) { return getKeyAccessor(userId, getKeyId()).getPreferredSymmetricKeyAlgorithms(); } + @Nonnull public Set getPreferredSymmetricKeyAlgorithms(long keyId) { return new KeyAccessor.SubKey(this, new SubkeyIdentifier(keys, keyId)).getPreferredSymmetricKeyAlgorithms(); } + @Nonnull public Set getPreferredCompressionAlgorithms() { return getPreferredCompressionAlgorithms(getPrimaryUserId()); } - public Set getPreferredCompressionAlgorithms(String userId) { + @Nonnull + public Set getPreferredCompressionAlgorithms(@Nullable CharSequence userId) { return getKeyAccessor(userId, getKeyId()).getPreferredCompressionAlgorithms(); } + @Nonnull public Set getPreferredCompressionAlgorithms(long keyId) { return new KeyAccessor.SubKey(this, new SubkeyIdentifier(keys, keyId)).getPreferredCompressionAlgorithms(); } @@ -1173,15 +1229,20 @@ public class KeyRingInfo { return true; } - private KeyAccessor getKeyAccessor(@Nullable String userId, long keyID) { + private KeyAccessor getKeyAccessor(@Nullable CharSequence userId, long keyID) { if (getPublicKey(keyID) == null) { throw new NoSuchElementException("No subkey with key id " + Long.toHexString(keyID) + " found on this key."); } - if (userId != null && !getUserIds().contains(userId)) { + + if (userId != null && !getUserIds().contains(userId.toString())) { throw new NoSuchElementException("No user-id '" + userId + "' found on this key."); } - return userId == null ? new KeyAccessor.ViaKeyId(this, new SubkeyIdentifier(keys, keyID)) - : new KeyAccessor.ViaUserId(this, new SubkeyIdentifier(keys, keyID), userId); + + if (userId != null) { + return new KeyAccessor.ViaUserId(this, new SubkeyIdentifier(keys, keyID), userId.toString()); + } else { + return new KeyAccessor.ViaKeyId(this, new SubkeyIdentifier(keys, keyID)); + } } public static class Signatures { @@ -1193,7 +1254,9 @@ public class KeyRingInfo { private final Map subkeyRevocations; private final Map subkeyBindings; - public Signatures(PGPKeyRing keyRing, Date referenceDate, Policy policy) { + public Signatures(@Nonnull PGPKeyRing keyRing, + @Nonnull Date referenceDate, + @Nonnull Policy policy) { primaryKeyRevocation = SignaturePicker.pickCurrentRevocationSelfSignature(keyRing, policy, referenceDate); primaryKeySelfSignature = SignaturePicker.pickLatestDirectKeySignature(keyRing, policy, referenceDate); userIdRevocations = new HashMap<>(); 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 01c09903..6210ef5b 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 @@ -72,14 +72,12 @@ public class SecretKeyRingEditor implements SecretKeyRingEditorInterface { private PGPSecretKeyRing secretKeyRing; private final Date referenceTime; - public SecretKeyRingEditor(PGPSecretKeyRing secretKeyRing) { - this(secretKeyRing, null); + public SecretKeyRingEditor(@Nonnull PGPSecretKeyRing secretKeyRing) { + this(secretKeyRing, new Date()); } - public SecretKeyRingEditor(PGPSecretKeyRing secretKeyRing, Date referenceTime) { - if (secretKeyRing == null) { - throw new NullPointerException("SecretKeyRing MUST NOT be null."); - } + public SecretKeyRingEditor(@Nonnull PGPSecretKeyRing secretKeyRing, + @Nonnull Date referenceTime) { this.secretKeyRing = secretKeyRing; this.referenceTime = referenceTime; } diff --git a/pgpainless-core/src/test/java/org/pgpainless/algorithm/RevocationStateTest.java b/pgpainless-core/src/test/java/org/pgpainless/algorithm/RevocationStateTest.java index e23d92d3..c24084b4 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/algorithm/RevocationStateTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/algorithm/RevocationStateTest.java @@ -60,11 +60,6 @@ public class RevocationStateTest { assertEquals("softRevoked (2022-08-03 18:26:35 UTC)", state.toString()); } - @Test - public void testSoftRevokedNullDateThrows() { - assertThrows(NullPointerException.class, () -> RevocationState.softRevoked(null)); - } - @Test public void orderTest() { assertEquals(RevocationState.notRevoked(), RevocationState.notRevoked()); diff --git a/pgpainless-core/src/test/java/org/pgpainless/key/info/UserIdRevocationTest.java b/pgpainless-core/src/test/java/org/pgpainless/key/info/UserIdRevocationTest.java index fd665901..0caf8b75 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/key/info/UserIdRevocationTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/key/info/UserIdRevocationTest.java @@ -95,6 +95,7 @@ public class UserIdRevocationTest { KeyRingInfo info = new KeyRingInfo(secretKeys); PGPSignature signature = info.getUserIdRevocation("secondary@key.id"); + assertNotNull(signature); RevocationReason reason = (RevocationReason) signature.getHashedSubPackets() .getSubpacket(SignatureSubpacketTags.REVOCATION_REASON); assertNotNull(reason); diff --git a/pgpainless-core/src/test/java/org/pgpainless/signature/SignatureSubpacketsUtilTest.java b/pgpainless-core/src/test/java/org/pgpainless/signature/SignatureSubpacketsUtilTest.java index d5cfb4f5..5b93415e 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/signature/SignatureSubpacketsUtilTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/signature/SignatureSubpacketsUtilTest.java @@ -145,6 +145,7 @@ public class SignatureSubpacketsUtilTest { PGPSignature signature = generator.generateCertification(secretKeys.getPublicKey()); Set featureSet = SignatureSubpacketsUtil.parseFeatures(signature); + assertNotNull(featureSet); assertEquals(2, featureSet.size()); assertTrue(featureSet.contains(Feature.MODIFICATION_DETECTION)); assertTrue(featureSet.contains(Feature.AEAD_ENCRYPTED_DATA)); @@ -216,6 +217,7 @@ public class SignatureSubpacketsUtilTest { PGPSignature signature = generator.generateCertification(secretKeys.getPublicKey()); RevocationKey revocationKey = SignatureSubpacketsUtil.getRevocationKey(signature); + assertNotNull(revocationKey); assertArrayEquals(secretKeys.getPublicKey().getFingerprint(), revocationKey.getFingerprint()); assertEquals(secretKeys.getPublicKey().getAlgorithm(), revocationKey.getAlgorithm()); } @@ -277,6 +279,7 @@ public class SignatureSubpacketsUtilTest { PGPSignature signature = generator.generateCertification(secretKeys.getPublicKey()); TrustSignature trustSignature = SignatureSubpacketsUtil.getTrustSignature(signature); + assertNotNull(trustSignature); assertEquals(10, trustSignature.getDepth()); assertEquals(3, trustSignature.getTrustAmount()); } diff --git a/pgpainless-core/src/test/java/org/pgpainless/signature/builder/ThirdPartyCertificationSignatureBuilderTest.java b/pgpainless-core/src/test/java/org/pgpainless/signature/builder/ThirdPartyCertificationSignatureBuilderTest.java index ced6a466..bf1cb694 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/signature/builder/ThirdPartyCertificationSignatureBuilderTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/signature/builder/ThirdPartyCertificationSignatureBuilderTest.java @@ -4,6 +4,7 @@ package org.pgpainless.signature.builder; +import org.bouncycastle.bcpg.sig.Exportable; import org.bouncycastle.openpgp.PGPException; import org.bouncycastle.openpgp.PGPPublicKeyRing; import org.bouncycastle.openpgp.PGPSecretKeyRing; @@ -22,6 +23,7 @@ import java.security.NoSuchAlgorithmException; import static org.junit.jupiter.api.Assertions.assertArrayEquals; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -61,7 +63,9 @@ public class ThirdPartyCertificationSignatureBuilderTest { assertEquals(SignatureType.GENERIC_CERTIFICATION, SignatureType.valueOf(certification.getSignatureType())); assertEquals(secretKeys.getPublicKey().getKeyID(), certification.getKeyID()); assertArrayEquals(secretKeys.getPublicKey().getFingerprint(), certification.getHashedSubPackets().getIssuerFingerprint().getFingerprint()); - assertFalse(SignatureSubpacketsUtil.getExportableCertification(certification).isExportable()); + Exportable exportable = SignatureSubpacketsUtil.getExportableCertification(certification); + assertNotNull(exportable); + assertFalse(exportable.isExportable()); // test sig correctness certification.init(ImplementationFactory.getInstance().getPGPContentVerifierBuilderProvider(), secretKeys.getPublicKey()); diff --git a/pgpainless-core/src/test/java/org/pgpainless/signature/subpackets/SignatureSubpacketsTest.java b/pgpainless-core/src/test/java/org/pgpainless/signature/subpackets/SignatureSubpacketsTest.java index 988bc776..3c2eda91 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/signature/subpackets/SignatureSubpacketsTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/signature/subpackets/SignatureSubpacketsTest.java @@ -376,7 +376,7 @@ public class SignatureSubpacketsTest { public void testSetSignatureTarget() { byte[] hash = new byte[20]; new Random().nextBytes(hash); - wrapper.setSignatureTarget(PublicKeyAlgorithm.fromId(key.getAlgorithm()), HashAlgorithm.SHA512, hash); + wrapper.setSignatureTarget(PublicKeyAlgorithm.requireFromId(key.getAlgorithm()), HashAlgorithm.SHA512, hash); PGPSignatureSubpacketVector vector = SignatureSubpacketsHelper.toVector(wrapper); SignatureTarget target = vector.getSignatureTarget(); From d05ffd0451f47633c7def76d6e6fc834c4cc492d Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 3 May 2023 16:11:06 +0200 Subject: [PATCH 291/763] Make DateUtil null-safe --- .../src/main/java/org/pgpainless/util/DateUtil.java | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/util/DateUtil.java b/pgpainless-core/src/main/java/org/pgpainless/util/DateUtil.java index ad1fce09..f56020d4 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/util/DateUtil.java +++ b/pgpainless-core/src/main/java/org/pgpainless/util/DateUtil.java @@ -4,6 +4,7 @@ package org.pgpainless.util; +import javax.annotation.Nonnull; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.Date; @@ -16,6 +17,7 @@ public final class DateUtil { } // Java's SimpleDateFormat is not thread-safe, therefore we return a new instance on every invocation. + @Nonnull public static SimpleDateFormat getParser() { SimpleDateFormat parser = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss z"); parser.setTimeZone(TimeZone.getTimeZone("UTC")); @@ -28,11 +30,12 @@ public final class DateUtil { * @param dateString timestamp * @return date */ - public static Date parseUTCDate(String dateString) { + @Nonnull + public static Date parseUTCDate(@Nonnull String dateString) { try { return getParser().parse(dateString); } catch (ParseException e) { - return null; + throw new IllegalArgumentException("Malformed UTC timestamp: " + dateString, e); } } @@ -42,6 +45,7 @@ public final class DateUtil { * @param date date * @return timestamp */ + @Nonnull public static String formatUTCDate(Date date) { return getParser().format(date); } @@ -51,7 +55,8 @@ public final class DateUtil { * @param date date * @return floored date */ - public static Date toSecondsPrecision(Date date) { + @Nonnull + public static Date toSecondsPrecision(@Nonnull Date date) { long millis = date.getTime(); long seconds = millis / 1000; long floored = seconds * 1000; @@ -63,6 +68,7 @@ public final class DateUtil { * * @return now */ + @Nonnull public static Date now() { return toSecondsPrecision(new Date()); } From 21ae48d8c11cc8686fb9c7b3d50ec02116f4a58d Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 3 May 2023 17:13:29 +0200 Subject: [PATCH 292/763] Use assert statements to flag impossible NPEs --- .../decryption_verification/OpenPgpMessageInputStream.java | 2 ++ .../pgpainless/key/certification/CertifyCertificate.java | 1 + .../key/modification/secretkeyring/SecretKeyRingEditor.java | 6 +++++- 3 files changed, 8 insertions(+), 1 deletion(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java index 04d823d1..07098269 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java @@ -544,6 +544,7 @@ public class OpenPgpMessageInputStream extends DecryptionStream { for (Tuple k : postponedDueToMissingPassphrase) { PGPSecretKey key = k.getA(); PGPSecretKeyRing keys = getDecryptionKey(key.getKeyID()); + assert (keys != null); keyIds.add(new SubkeyIdentifier(keys, key.getKeyID())); } if (!keyIds.isEmpty()) { @@ -556,6 +557,7 @@ public class OpenPgpMessageInputStream extends DecryptionStream { PGPSecretKey secretKey = missingPassphrases.getA(); long keyId = secretKey.getKeyID(); PGPSecretKeyRing decryptionKey = getDecryptionKey(keyId); + assert (decryptionKey != null); SubkeyIdentifier decryptionKeyId = new SubkeyIdentifier(decryptionKey, keyId); if (hasUnsupportedS2KSpecifier(secretKey, decryptionKeyId)) { continue; diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/certification/CertifyCertificate.java b/pgpainless-core/src/main/java/org/pgpainless/key/certification/CertifyCertificate.java index 05e64879..9340d93d 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/key/certification/CertifyCertificate.java +++ b/pgpainless-core/src/main/java/org/pgpainless/key/certification/CertifyCertificate.java @@ -275,6 +275,7 @@ public class CertifyCertificate { // We only support certification-capable primary keys OpenPgpFingerprint fingerprint = info.getFingerprint(); PGPPublicKey certificationPubKey = info.getPublicKey(fingerprint); + assert (certificationPubKey != null); if (!info.isKeyValidlyBound(certificationPubKey.getKeyID())) { throw new KeyException.RevokedKeyException(fingerprint); } 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 6210ef5b..73c39c25 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 @@ -182,7 +182,10 @@ public class SecretKeyRingEditor implements SecretKeyRingEditorInterface { } // We need to unmark this user-id as primary - if (info.getLatestUserIdCertification(otherUserId).getHashedSubPackets().isPrimaryUserID()) { + PGPSignature userIdCertification = info.getLatestUserIdCertification(otherUserId); + assert (userIdCertification != null); + + if (userIdCertification.getHashedSubPackets().isPrimaryUserID()) { addUserId(otherUserId, new SelfSignatureSubpackets.Callback() { @Override public void modifyHashedSubpackets(SelfSignatureSubpackets hashedSubpackets) { @@ -601,6 +604,7 @@ public class SecretKeyRingEditor implements SecretKeyRingEditorInterface { } if (prevUserIdSig.getHashedSubPackets().isPrimaryUserID()) { + assert (primaryUserId != null); PGPSignature userIdSig = reissueNonPrimaryUserId(secretKeyRingProtector, userId, prevUserIdSig); secretKeyRing = KeyRingUtils.injectCertification(secretKeyRing, primaryUserId, userIdSig); } From 09bacd40d1701d52dbb23d3360c7dca1532e2000 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 3 May 2023 17:14:18 +0200 Subject: [PATCH 293/763] SecretKeyRingEditor: referenceTime cannot be null anymore --- .../secretkeyring/SecretKeyRingEditor.java | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) 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 73c39c25..7c577600 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 @@ -124,9 +124,7 @@ public class SecretKeyRingEditor implements SecretKeyRingEditorInterface { } SelfSignatureBuilder builder = new SelfSignatureBuilder(primaryKey, protector); - if (referenceTime != null) { - builder.getHashedSubpackets().setSignatureCreationTime(referenceTime); - } + builder.getHashedSubpackets().setSignatureCreationTime(referenceTime); builder.setSignatureType(SignatureType.POSITIVE_CERTIFICATION); // Retain signature subpackets of previous signatures @@ -351,16 +349,12 @@ public class SecretKeyRingEditor implements SecretKeyRingEditorInterface { .getV4FingerprintCalculator(), false, subkeyProtector.getEncryptor(subkey.getKeyID())); SubkeyBindingSignatureBuilder skBindingBuilder = new SubkeyBindingSignatureBuilder(primaryKey, primaryKeyProtector, hashAlgorithm); - if (referenceTime != null) { - skBindingBuilder.getHashedSubpackets().setSignatureCreationTime(referenceTime); - } + skBindingBuilder.getHashedSubpackets().setSignatureCreationTime(referenceTime); skBindingBuilder.getHashedSubpackets().setKeyFlags(flags); if (subkeyAlgorithm.isSigningCapable()) { PrimaryKeyBindingSignatureBuilder pkBindingBuilder = new PrimaryKeyBindingSignatureBuilder(secretSubkey, subkeyProtector, hashAlgorithm); - if (referenceTime != null) { - pkBindingBuilder.getHashedSubpackets().setSignatureCreationTime(referenceTime); - } + pkBindingBuilder.getHashedSubpackets().setSignatureCreationTime(referenceTime); PGPSignature pkBinding = pkBindingBuilder.build(primaryKey.getPublicKey()); skBindingBuilder.getHashedSubpackets().addEmbeddedSignature(pkBinding); } From 7a194c517ac9ce03d3e9eaffc770c12a1655580a Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 3 May 2023 17:15:30 +0200 Subject: [PATCH 294/763] Remove KeyRingUtils.removeSecretKey() in favor of stripSecretKey() --- .../org/pgpainless/key/util/KeyRingUtils.java | 24 ------------------- .../CertificateWithMissingSecretKeyTest.java | 2 +- 2 files changed, 1 insertion(+), 25 deletions(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/util/KeyRingUtils.java b/pgpainless-core/src/main/java/org/pgpainless/key/util/KeyRingUtils.java index 2ce058fd..23361c13 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/key/util/KeyRingUtils.java +++ b/pgpainless-core/src/main/java/org/pgpainless/key/util/KeyRingUtils.java @@ -440,30 +440,6 @@ public final class KeyRingUtils { return newSecretKey; } - /** - * Remove the secret key of the subkey identified by the given secret key id from the key ring. - * The public part stays attached to the key ring, so that it can still be used for encryption / verification of signatures. - * - * This method is intended to be used to remove secret primary keys from live keys when those are kept in offline storage. - * - * @param secretKeys secret key ring - * @param secretKeyId id of the secret key to remove - * @return secret key ring with removed secret key - * - * @throws IOException in case of an error during serialization / deserialization of the key - * @throws PGPException in case of a broken key - * - * @deprecated use {@link #stripSecretKey(PGPSecretKeyRing, long)} instead. - * TODO: Remove in 1.2.X - */ - @Nonnull - @Deprecated - public static PGPSecretKeyRing removeSecretKey(@Nonnull PGPSecretKeyRing secretKeys, - long secretKeyId) - throws IOException, PGPException { - return stripSecretKey(secretKeys, secretKeyId); - } - /** * Remove the secret key of the subkey identified by the given secret key id from the key ring. * The public part stays attached to the key ring, so that it can still be used for encryption / verification of signatures. diff --git a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/CertificateWithMissingSecretKeyTest.java b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/CertificateWithMissingSecretKeyTest.java index a53999a6..e5f9e370 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/CertificateWithMissingSecretKeyTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/CertificateWithMissingSecretKeyTest.java @@ -83,7 +83,7 @@ public class CertificateWithMissingSecretKeyTest { encryptionSubkeyId = PGPainless.inspectKeyRing(secretKeys) .getEncryptionSubkeys(EncryptionPurpose.ANY).get(0).getKeyID(); // remove the encryption/decryption secret key - missingDecryptionSecKey = KeyRingUtils.removeSecretKey(secretKeys, encryptionSubkeyId); + missingDecryptionSecKey = KeyRingUtils.stripSecretKey(secretKeys, encryptionSubkeyId); } @Test From 5c76f9046f28f013bf3e3ce70c7ac8f092abec6f Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 3 May 2023 17:16:10 +0200 Subject: [PATCH 295/763] Turn empty catch block into test failure --- .../IgnoreUnknownSignatureVersionsTest.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/IgnoreUnknownSignatureVersionsTest.java b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/IgnoreUnknownSignatureVersionsTest.java index 2b60ea02..aa1da741 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/IgnoreUnknownSignatureVersionsTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/IgnoreUnknownSignatureVersionsTest.java @@ -5,6 +5,7 @@ package org.pgpainless.decryption_verification; import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; import java.io.ByteArrayInputStream; import java.io.IOException; @@ -71,7 +72,7 @@ public class IgnoreUnknownSignatureVersionsTest { try { cert = PGPainless.readKeyRing().publicKeyRing(CERT); } catch (IOException e) { - + fail("Cannot parse certificate.", e); } } From 78cb2ec3d0f3b14ff16ff87d4dc5343c803b6c8e Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 3 May 2023 17:16:56 +0200 Subject: [PATCH 296/763] Do not catch and immediatelly rethrow exception --- .../decryption_verification/TeeBCPGInputStream.java | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/TeeBCPGInputStream.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/TeeBCPGInputStream.java index bdbd9bcd..e1d33f70 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/TeeBCPGInputStream.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/TeeBCPGInputStream.java @@ -7,7 +7,6 @@ package org.pgpainless.decryption_verification; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; -import java.util.NoSuchElementException; import org.bouncycastle.bcpg.BCPGInputStream; import org.bouncycastle.bcpg.MarkerPacket; @@ -49,13 +48,7 @@ public class TeeBCPGInputStream { return null; } - OpenPgpPacket packet; - try { - packet = OpenPgpPacket.requireFromTag(tag); - } catch (NoSuchElementException e) { - throw e; - } - return packet; + return OpenPgpPacket.requireFromTag(tag); } public Packet readPacket() throws IOException { From 3cea98536506dfa2a44d5898c27e096cd53cdfb9 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 3 May 2023 17:19:18 +0200 Subject: [PATCH 297/763] TeeBCPGInputStream: Annotate byte[] arg as @Nonnull --- .../decryption_verification/TeeBCPGInputStream.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/TeeBCPGInputStream.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/TeeBCPGInputStream.java index e1d33f70..725c6f6e 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/TeeBCPGInputStream.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/TeeBCPGInputStream.java @@ -19,6 +19,8 @@ import org.bouncycastle.openpgp.PGPOnePassSignature; import org.bouncycastle.openpgp.PGPSignature; import org.pgpainless.algorithm.OpenPgpPacket; +import javax.annotation.Nonnull; + /** * Since we need to update signatures with data from the underlying stream, this class is used to tee out the data. * Unfortunately we cannot simply override {@link BCPGInputStream#read()} to tee the data out though, since @@ -121,7 +123,7 @@ public class TeeBCPGInputStream { } @Override - public int read(byte[] b, int off, int len) throws IOException { + public int read(@Nonnull byte[] b, int off, int len) throws IOException { if (last != -1) { outputStream.write(last); } From fb581f11c792c17f516ec50683b50bcdad24ca53 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 3 May 2023 17:20:02 +0200 Subject: [PATCH 298/763] UserId.parse(): Prevent self-referencing javadoc --- .../src/main/java/org/pgpainless/key/util/UserId.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/util/UserId.java b/pgpainless-core/src/main/java/org/pgpainless/key/util/UserId.java index 427f9060..b115afda 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/key/util/UserId.java +++ b/pgpainless-core/src/main/java/org/pgpainless/key/util/UserId.java @@ -88,7 +88,7 @@ public final class UserId implements CharSequence { *

    • John Doe <john@pgpainless.org>
    • *
    • John Doe (work email) <john@pgpainless.org>
    • *
    - * In these cases, {@link #parse(String)} will detect email addresses, names and comments and expose those + * In these cases, this method will detect email addresses, names and comments and expose those * via the respective getters. * This method does not support parsing mail addresses of the following formats: *