From 5869996059aa7cca71d31ca4dbaced3fee43e6ce Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Fri, 1 Oct 2021 14:12:10 +0200 Subject: [PATCH] Add isSignedOnly() to MessageInspector This method can be used to determine, whether the message was created using gpg --sign --armor. It will return false if the message is signed and encrypted, since we cannot determine signed status while the message is encrypted. Fixes #188 --- .../MessageInspector.java | 38 ++++++++++++-- .../MessageInspectorTest.java | 50 +++++++++++++++++++ 2 files changed, 85 insertions(+), 3 deletions(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/MessageInspector.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/MessageInspector.java index 7fc54810..1cb98b38 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/MessageInspector.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/MessageInspector.java @@ -27,6 +27,7 @@ import org.bouncycastle.openpgp.PGPEncryptedDataList; import org.bouncycastle.openpgp.PGPException; import org.bouncycastle.openpgp.PGPLiteralData; import org.bouncycastle.openpgp.PGPObjectFactory; +import org.bouncycastle.openpgp.PGPOnePassSignatureList; import org.bouncycastle.openpgp.PGPPBEEncryptedData; import org.bouncycastle.openpgp.PGPPublicKeyEncryptedData; import org.pgpainless.implementation.ImplementationFactory; @@ -40,7 +41,12 @@ public final class MessageInspector { public static class EncryptionInfo { private final List keyIds = new ArrayList<>(); private boolean isPassphraseEncrypted = false; + private boolean isSignedOnly = false; + /** + * Return a list of recipient key ids for whom the message is encrypted. + * @return recipient key ids + */ public List getKeyIds() { return Collections.unmodifiableList(keyIds); } @@ -48,6 +54,24 @@ public final class MessageInspector { public boolean isPassphraseEncrypted() { return isPassphraseEncrypted; } + + /** + * Return true, if the message is encrypted. + * + * @return true if encrypted + */ + public boolean isEncrypted() { + return isPassphraseEncrypted || !keyIds.isEmpty(); + } + + /** + * Return true, if the message is not encrypted, but signed using {@link org.bouncycastle.openpgp.PGPOnePassSignature OnePassSignatures}. + * + * @return true if message is signed only + */ + public boolean isSignedOnly() { + return isSignedOnly; + } } private MessageInspector() { @@ -67,16 +91,24 @@ public final class MessageInspector { InputStream decoded = ArmorUtils.getDecoderStream(dataIn); EncryptionInfo info = new EncryptionInfo(); - collectDecryptionKeyIDs(decoded, info); + processMessage(decoded, info); return info; } - private static void collectDecryptionKeyIDs(InputStream dataIn, EncryptionInfo info) throws PGPException { + private static void processMessage(InputStream dataIn, EncryptionInfo info) throws PGPException { PGPObjectFactory objectFactory = new PGPObjectFactory(dataIn, ImplementationFactory.getInstance().getKeyFingerprintCalculator()); for (Object next : objectFactory) { + if (next instanceof PGPOnePassSignatureList) { + PGPOnePassSignatureList signatures = (PGPOnePassSignatureList) next; + if (!signatures.isEmpty()) { + info.isSignedOnly = true; + return; + } + } + if (next instanceof PGPEncryptedDataList) { PGPEncryptedDataList encryptedDataList = (PGPEncryptedDataList) next; for (PGPEncryptedData encryptedData : encryptedDataList) { @@ -92,7 +124,7 @@ public final class MessageInspector { if (next instanceof PGPCompressedData) { PGPCompressedData compressed = (PGPCompressedData) next; InputStream decompressed = compressed.getDataStream(); - collectDecryptionKeyIDs(decompressed, info); + processMessage(decompressed, info); } if (next instanceof PGPLiteralData) { diff --git a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/MessageInspectorTest.java b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/MessageInspectorTest.java index ff31ae7c..8ffb1cc9 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/MessageInspectorTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/MessageInspectorTest.java @@ -44,6 +44,8 @@ public class MessageInspectorTest { new ByteArrayInputStream(message.getBytes(StandardCharsets.UTF_8))); assertFalse(info.isPassphraseEncrypted()); + assertFalse(info.isSignedOnly()); + assertTrue(info.isEncrypted()); assertEquals(1, info.getKeyIds().size()); assertEquals(KeyIdUtil.fromLongKeyId("4766F6B9D5F21EB6"), info.getKeyIds().get(0)); } @@ -66,9 +68,57 @@ public class MessageInspectorTest { MessageInspector.EncryptionInfo info = MessageInspector.determineEncryptionInfoForMessage(new ByteArrayInputStream(message.getBytes(StandardCharsets.UTF_8))); + assertTrue(info.isEncrypted()); assertTrue(info.isPassphraseEncrypted()); assertEquals(2, info.getKeyIds().size()); + assertFalse(info.isSignedOnly()); assertTrue(info.getKeyIds().contains(KeyIdUtil.fromLongKeyId("4C6E8F99F6E47184"))); assertTrue(info.getKeyIds().contains(KeyIdUtil.fromLongKeyId("1839079A640B2FAC"))); } + + @Test + public void testSignedOnlyMessage() throws PGPException, IOException { + String message = "-----BEGIN PGP MESSAGE-----\n" + + "Version: PGPainless\n" + + "\n" + + "owGbwMvMwCU2JftV+VJxWRbG0yJJDCDgkZqTk6+jEJ5flJOiyNVRysIgxsXAxsqU\n" + + "GPbzCoMipwBMg5giy+9JdusX5zywTwq60QsTfj2J4a9ki6nKuVnu940q5qzl+aK3\n" + + "89zdHzzyDBEdJg4asQcf3PBk+Cu1W/vQ1mMVW3fyTVc0VNe9PyktZlfcge2CbR8F\n" + + "Dvxwv8UPAA==\n" + + "=nt5n\n" + + "-----END PGP MESSAGE-----"; + + MessageInspector.EncryptionInfo info = MessageInspector.determineEncryptionInfoForMessage(new ByteArrayInputStream(message.getBytes(StandardCharsets.UTF_8))); + + assertTrue(info.isSignedOnly()); + + assertFalse(info.isEncrypted()); + assertFalse(info.isPassphraseEncrypted()); + assertEquals(0, info.getKeyIds().size()); + } + + @Test + public void testEncryptedAndSignedMessage() throws PGPException, IOException { + String message = "-----BEGIN PGP MESSAGE-----\n" + + "Version: PGPainless\n" + + "\n" + + "jC4ECQMCKFdrpiMTt8xgtZjkH60Nu4s+5THbPWOgyTmXkAeBAsmXDNWWuB5QSXFz\n" + + "hF4DaM8R0fnHE6USAQdALJl6fCtB597Ub/GR3bxu3Uv2lirMA8bI2iGHUE7f0Rkw\n" + + "ZNgmEk3YRGp+zddZoLp0WAIL0y4FLwUlMrR+YFYA37eAILiCwLEesIpvIoYq+fIu\n" + + "0r4BJ/bM9oiCZGy7clpBQgOBFOTMR2fCO9ESVOaLwTGDJkVk6m+iLV1OYG6997vP\n" + + "qHrg/zzy/U+xm90iHJzXoQ7yd2QZMU7llvC/otf5j14x3PCqd/rIxQrO2uc76Pef\n" + + "Lh1JRHb7St4PC429HfE7pEAfFUej1I56U/ZCPwxa9f6je911jM4ZmZQTKJq3XZ3H\n" + + "KK0Ymg5GrsBTEGFm4jb1p+V85PPhsIioX3np/N3fkIfxFguTGZza33/GHy61+DTy\n" + + "=SZU6\n" + + "-----END PGP MESSAGE-----"; + MessageInspector.EncryptionInfo info = MessageInspector.determineEncryptionInfoForMessage(new ByteArrayInputStream(message.getBytes(StandardCharsets.UTF_8))); + + // Message is encrypted, so we cannot determine if it is signed or not. + // It is not signed only + assertFalse(info.isSignedOnly()); + + assertTrue(info.isEncrypted()); + assertTrue(info.isPassphraseEncrypted()); + assertEquals(1, info.getKeyIds().size()); + } }