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 1884ee92..a80d1fee 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 @@ -53,6 +53,7 @@ public class ConsumerOptions { private MissingKeyPassphraseStrategy missingKeyPassphraseStrategy = MissingKeyPassphraseStrategy.INTERACTIVE; private MultiPassStrategy multiPassStrategy = new InMemoryMultiPassStrategy(); + private boolean cleartextSigned; /** * Consider signatures on the message made before the given timestamp invalid. @@ -352,4 +353,20 @@ public class ConsumerOptions { public MultiPassStrategy getMultiPassStrategy() { return multiPassStrategy; } + + /** + * INTERNAL method to mark cleartext signed messages. + * Do not call this manually. + */ + public void setIsCleartextSigned() { + this.cleartextSigned = true; + } + + /** + * Return true if the message is cleartext signed. + * @return cleartext signed + */ + public boolean isCleartextSigned() { + return this.cleartextSigned; + } } 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 72707037..560a4d39 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 @@ -16,6 +16,7 @@ import java.util.List; import java.util.Map; import java.util.Set; import javax.annotation.Nonnull; +import javax.annotation.Nullable; import org.bouncycastle.bcpg.ArmoredInputStream; import org.bouncycastle.openpgp.PGPCompressedData; @@ -126,6 +127,12 @@ public final class DecryptionStreamFactory { InputStream decoderStream; PGPObjectFactory objectFactory; + if (options.isCleartextSigned()) { + inputStream = wrapInVerifySignatureStream(bufferedIn, null); + return new DecryptionStream(inputStream, resultBuilder, integrityProtectedEncryptedInputStream, + null); + } + try { decoderStream = PGPUtilWrapper.getDecoderStream(bufferedIn); decoderStream = CRCingArmoredInputStreamWrapper.possiblyWrap(decoderStream); @@ -170,7 +177,7 @@ public final class DecryptionStreamFactory { (decoderStream instanceof ArmoredInputStream) ? decoderStream : null); } - private InputStream wrapInVerifySignatureStream(InputStream bufferedIn, PGPObjectFactory objectFactory) { + private InputStream wrapInVerifySignatureStream(InputStream bufferedIn, @Nullable PGPObjectFactory objectFactory) { return new SignatureInputStream.VerifySignatures( bufferedIn, objectFactory, onePassSignatureChecks, onePassSignaturesWithMissingCert, detachedSignatureChecks, options, 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 cd8682df..44a4a468 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 @@ -12,6 +12,7 @@ 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; @@ -46,7 +47,7 @@ public abstract class SignatureInputStream extends FilterInputStream { public VerifySignatures( InputStream literalDataStream, - PGPObjectFactory objectFactory, + @Nullable PGPObjectFactory objectFactory, List opSignatures, Map onePassSignaturesWithMissingCert, List detachedSignatures, @@ -93,6 +94,9 @@ public abstract class SignatureInputStream extends FilterInputStream { } public void parseAndCombineSignatures() throws IOException { + if (objectFactory == null) { + return; + } // Parse signatures from message PGPSignatureList signatures; try { diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/cleartext_signatures/CleartextSignatureProcessor.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/cleartext_signatures/CleartextSignatureProcessor.java index 87facea7..352321ca 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/cleartext_signatures/CleartextSignatureProcessor.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/cleartext_signatures/CleartextSignatureProcessor.java @@ -6,7 +6,6 @@ package org.pgpainless.decryption_verification.cleartext_signatures; import java.io.IOException; import java.io.InputStream; -import java.util.logging.Logger; import org.bouncycastle.bcpg.ArmoredInputStream; import org.bouncycastle.openpgp.PGPException; @@ -27,8 +26,6 @@ import org.pgpainless.util.ArmoredInputStreamFactory; */ public class CleartextSignatureProcessor { - private static final Logger LOGGER = Logger.getLogger(CleartextSignatureProcessor.class.getName()); - private final ArmoredInputStream in; private final ConsumerOptions options; @@ -71,6 +68,7 @@ public class CleartextSignatureProcessor { options.addVerificationOfDetachedSignature(signature); } + options.setIsCleartextSigned(); return PGPainless.decryptAndOrVerify() .onInputStream(multiPassStrategy.getMessageInputStream()) .withOptions(options); diff --git a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/CleartextSignatureVerificationTest.java b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/CleartextSignatureVerificationTest.java index 0fa5e9e4..7ca8592f 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/CleartextSignatureVerificationTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/CleartextSignatureVerificationTest.java @@ -15,6 +15,9 @@ import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.nio.charset.StandardCharsets; +import java.security.InvalidAlgorithmParameterException; +import java.security.NoSuchAlgorithmException; +import java.util.Random; import org.bouncycastle.openpgp.PGPException; import org.bouncycastle.openpgp.PGPPublicKey; @@ -72,6 +75,9 @@ public class CleartextSignatureVerificationTest { "=Z2SO\n" + "-----END PGP SIGNATURE-----").getBytes(StandardCharsets.UTF_8); + public static final String alphabet = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; + public static final Random random = new Random(); + @Test public void cleartextSignVerification_InMemoryMultiPassStrategy() throws IOException, PGPException { PGPPublicKeyRing signingKeys = TestKeys.getEmilPublicKeyRing(); @@ -228,4 +234,55 @@ public class CleartextSignatureVerificationTest { OpenPgpMetadata metadata = verificationStream.getResult(); assertTrue(metadata.isVerified()); } + + @Test + public void testDecryptionOfVeryLongClearsignedMessage() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, IOException { + String message = randomString(28, 4000); + + PGPSecretKeyRing secretKeys = PGPainless.generateKeyRing().modernKeyRing("Alice", null); + ByteArrayOutputStream out = new ByteArrayOutputStream(); + EncryptionStream encryptionStream = PGPainless.encryptAndOrSign() + .onOutputStream(out) + .withOptions(ProducerOptions.sign( + SigningOptions.get() + .addDetachedSignature(SecretKeyRingProtector.unprotectedKeys(), + secretKeys, DocumentSignatureType.CANONICAL_TEXT_DOCUMENT) + ).setCleartextSigned()); + + Streams.pipeAll(new ByteArrayInputStream(message.getBytes(StandardCharsets.UTF_8)), encryptionStream); + encryptionStream.close(); + + String cleartextSigned = out.toString(); + + ByteArrayInputStream in = new ByteArrayInputStream(cleartextSigned.getBytes(StandardCharsets.UTF_8)); + DecryptionStream decryptionStream = PGPainless.decryptAndOrVerify() + .onInputStream(in) + .withOptions(new ConsumerOptions() + .addVerificationCert(PGPainless.extractCertificate(secretKeys))); + + out = new ByteArrayOutputStream(); + Streams.pipeAll(decryptionStream, out); + decryptionStream.close(); + } + + private String randomString(int maxWordLen, int wordCount) { + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < wordCount; i++) { + sb.append(randomWord(maxWordLen)).append(' '); + int n = random.nextInt(12); + if (n == 11) { + sb.append('\n'); + } + } + return sb.toString(); + } + + private String randomWord(int maxWordLen) { + int len = random.nextInt(maxWordLen); + char[] word = new char[len]; + for (int i = 0; i < word.length; i++) { + word[i] = alphabet.charAt(random.nextInt(alphabet.length())); + } + return new String(word); + } }