From 59c9ec341edd2b0fd2b26678aff3dcb5f7668e95 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 2 Nov 2021 12:12:29 +0100 Subject: [PATCH] Hide distinction between clearsigned and inline signed message verification --- .../main/java/org/pgpainless/PGPainless.java | 22 ---------- .../ConsumerOptions.java | 25 +++++++++++ .../DecryptionBuilder.java | 25 +++++++++-- .../DecryptionStreamFactory.java | 3 +- .../CleartextSignatureProcessor.java | 6 +-- .../VerifyCleartextSignatures.java | 19 +-------- .../VerifyCleartextSignaturesImpl.java | 19 ++------- .../CleartextSignatureVerificationTest.java | 42 ++++++------------- .../pgpainless/example/DecryptOrVerify.java | 23 ++-------- 9 files changed, 69 insertions(+), 115 deletions(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/PGPainless.java b/pgpainless-core/src/main/java/org/pgpainless/PGPainless.java index 7d5ca4b0..5bbe0e14 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/PGPainless.java +++ b/pgpainless-core/src/main/java/org/pgpainless/PGPainless.java @@ -22,8 +22,6 @@ import org.pgpainless.key.modification.secretkeyring.SecretKeyRingEditorInterfac import org.pgpainless.key.parsing.KeyRingReader; import org.pgpainless.key.util.KeyRingUtils; import org.pgpainless.policy.Policy; -import org.pgpainless.decryption_verification.cleartext_signatures.VerifyCleartextSignatures; -import org.pgpainless.decryption_verification.cleartext_signatures.VerifyCleartextSignaturesImpl; import org.pgpainless.util.ArmorUtils; public final class PGPainless { @@ -91,26 +89,6 @@ public final class PGPainless { return new DecryptionBuilder(); } - /** - * Verify a cleartext-signed message. - * Cleartext signed messages are often found in emails and look like this: - *
-     * {@code
-     * -----BEGIN PGP SIGNED MESSAGE-----
-     * Hash: [Hash algorithm]
-     * [Human Readable Message Body]
-     * -----BEGIN PGP SIGNATURE-----
-     * [Signature]
-     * -----END PGP SIGNATURE-----
-     * }
-     * 
- * - * @return builder - */ - public static VerifyCleartextSignatures verifyCleartextSignedMessage() { - return new VerifyCleartextSignaturesImpl(); - } - /** * Make changes to a key ring. * This method can be used to change key expiration dates and passphrases, or add/remove/revoke subkeys. 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 3c1a9454..1884ee92 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,8 @@ import org.bouncycastle.openpgp.PGPPublicKeyRingCollection; import org.bouncycastle.openpgp.PGPSecretKeyRing; import org.bouncycastle.openpgp.PGPSecretKeyRingCollection; import org.bouncycastle.openpgp.PGPSignature; +import org.pgpainless.decryption_verification.cleartext_signatures.InMemoryMultiPassStrategy; +import org.pgpainless.decryption_verification.cleartext_signatures.MultiPassStrategy; import org.pgpainless.exception.NotYetImplementedException; import org.pgpainless.key.protection.SecretKeyRingProtector; import org.pgpainless.signature.SignatureUtils; @@ -50,6 +52,7 @@ public class ConsumerOptions { private final Set decryptionPassphrases = new HashSet<>(); private MissingKeyPassphraseStrategy missingKeyPassphraseStrategy = MissingKeyPassphraseStrategy.INTERACTIVE; + private MultiPassStrategy multiPassStrategy = new InMemoryMultiPassStrategy(); /** * Consider signatures on the message made before the given timestamp invalid. @@ -327,4 +330,26 @@ public class ConsumerOptions { MissingKeyPassphraseStrategy getMissingKeyPassphraseStrategy() { return missingKeyPassphraseStrategy; } + + /** + * Set a custom multi-pass strategy for processing cleartext-signed messages. + * Uses {@link InMemoryMultiPassStrategy} by default. + * + * @param multiPassStrategy multi-pass caching strategy + * @return builder + */ + public ConsumerOptions setMultiPassStrategy(@Nonnull MultiPassStrategy multiPassStrategy) { + this.multiPassStrategy = multiPassStrategy; + return this; + } + + /** + * Return the currently configured {@link MultiPassStrategy}. + * Defaults to {@link InMemoryMultiPassStrategy}. + * + * @return multi-pass strategy + */ + public MultiPassStrategy getMultiPassStrategy() { + return multiPassStrategy; + } } 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 6dcc355b..0611ae99 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 @@ -4,31 +4,48 @@ package org.pgpainless.decryption_verification; +import java.io.BufferedInputStream; import java.io.IOException; import java.io.InputStream; import javax.annotation.Nonnull; import org.bouncycastle.openpgp.PGPException; +import org.pgpainless.decryption_verification.cleartext_signatures.VerifyCleartextSignaturesImpl; +import org.pgpainless.exception.WrongConsumingMethodException; public class DecryptionBuilder implements DecryptionBuilderInterface { - private InputStream inputStream; + public static int BUFFER_SIZE = 4096; @Override public DecryptWith onInputStream(@Nonnull InputStream inputStream) { - this.inputStream = inputStream; - return new DecryptWithImpl(); + return new DecryptWithImpl(inputStream); } class DecryptWithImpl implements DecryptWith { + private BufferedInputStream inputStream; + + DecryptWithImpl(InputStream inputStream) { + this.inputStream = new BufferedInputStream(inputStream, BUFFER_SIZE); + this.inputStream.mark(BUFFER_SIZE); + } + @Override public DecryptionStream withOptions(ConsumerOptions consumerOptions) throws PGPException, IOException { if (consumerOptions == null) { throw new IllegalArgumentException("Consumer options cannot be null."); } - return DecryptionStreamFactory.create(inputStream, consumerOptions); + try { + return DecryptionStreamFactory.create(inputStream, consumerOptions); + } catch (WrongConsumingMethodException e) { + inputStream.reset(); + return new VerifyCleartextSignaturesImpl() + .onInputStream(inputStream) + .withOptions(consumerOptions) + .getVerificationStream(); + } } } } 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 ad7088f1..66ed2d05 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 @@ -121,7 +121,8 @@ public final class DecryptionStreamFactory { private DecryptionStream parseOpenPGPDataAndCreateDecryptionStream(InputStream inputStream) throws IOException, PGPException { // Make sure we handle armored and non-armored data properly - BufferedInputStream bufferedIn = new BufferedInputStream(inputStream); + BufferedInputStream bufferedIn = new BufferedInputStream(inputStream, 512); + bufferedIn.mark(512); InputStream decoderStream; PGPObjectFactory objectFactory; 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 636b78e4..87facea7 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 @@ -31,11 +31,9 @@ public class CleartextSignatureProcessor { private final ArmoredInputStream in; private final ConsumerOptions options; - private final MultiPassStrategy multiPassStrategy; public CleartextSignatureProcessor(InputStream inputStream, - ConsumerOptions options, - MultiPassStrategy multiPassStrategy) + ConsumerOptions options) throws IOException { if (inputStream instanceof ArmoredInputStream) { this.in = (ArmoredInputStream) inputStream; @@ -43,7 +41,6 @@ public class CleartextSignatureProcessor { this.in = ArmoredInputStreamFactory.get(inputStream); } this.options = options; - this.multiPassStrategy = multiPassStrategy; } /** @@ -67,6 +64,7 @@ public class CleartextSignatureProcessor { .setSymmetricKeyAlgorithm(SymmetricKeyAlgorithm.NULL) .setFileEncoding(StreamEncoding.TEXT); + MultiPassStrategy multiPassStrategy = options.getMultiPassStrategy(); PGPSignatureList signatures = ClearsignedMessageUtil.detachSignaturesFromInbandClearsignedMessage(in, multiPassStrategy.getMessageOutputStream()); for (PGPSignature signature : signatures) { diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/cleartext_signatures/VerifyCleartextSignatures.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/cleartext_signatures/VerifyCleartextSignatures.java index 31317c6c..52360869 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/cleartext_signatures/VerifyCleartextSignatures.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/cleartext_signatures/VerifyCleartextSignatures.java @@ -4,7 +4,6 @@ package org.pgpainless.decryption_verification.cleartext_signatures; -import java.io.File; import java.io.IOException; import java.io.InputStream; @@ -20,23 +19,7 @@ public interface VerifyCleartextSignatures { * @param inputStream inputstream * @return api handle */ - WithStrategy onInputStream(InputStream inputStream); - - interface WithStrategy { - - /** - * Provide a {@link MultiPassStrategy} which is used to store the message content. - * Since cleartext-signed messages cannot be processed in one pass, the message has to be passed twice. - * Therefore the user needs to decide upon a strategy where to cache/store the message between the passes. - * This could be {@link MultiPassStrategy#writeMessageToFile(File)} or {@link MultiPassStrategy#keepMessageInMemory()}, - * depending on message size and use-case. - * - * @param multiPassStrategy strategy - * @return api handle - */ - VerifyWith withStrategy(MultiPassStrategy multiPassStrategy); - - } + VerifyWith onInputStream(InputStream inputStream); interface VerifyWith { diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/cleartext_signatures/VerifyCleartextSignaturesImpl.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/cleartext_signatures/VerifyCleartextSignaturesImpl.java index 276e027f..fde90874 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/cleartext_signatures/VerifyCleartextSignaturesImpl.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/cleartext_signatures/VerifyCleartextSignaturesImpl.java @@ -12,31 +12,18 @@ import org.pgpainless.decryption_verification.ConsumerOptions; public class VerifyCleartextSignaturesImpl implements VerifyCleartextSignatures { private InputStream inputStream; - private MultiPassStrategy multiPassStrategy; @Override - public WithStrategy onInputStream(InputStream inputStream) { + public VerifyWithImpl onInputStream(InputStream inputStream) { VerifyCleartextSignaturesImpl.this.inputStream = inputStream; - return new WithStrategyImpl(); - } - - public class WithStrategyImpl implements WithStrategy { - - @Override - public VerifyWith withStrategy(MultiPassStrategy multiPassStrategy) { - if (multiPassStrategy == null) { - throw new NullPointerException("MultiPassStrategy cannot be null."); - } - VerifyCleartextSignaturesImpl.this.multiPassStrategy = multiPassStrategy; - return new VerifyWithImpl(); - } + return new VerifyWithImpl(); } public class VerifyWithImpl implements VerifyWith { @Override public CleartextSignatureProcessor withOptions(ConsumerOptions options) throws IOException { - return new CleartextSignatureProcessor(inputStream, options, multiPassStrategy); + return new CleartextSignatureProcessor(inputStream, 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 c355438d..cdba4d07 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 @@ -25,9 +25,9 @@ import org.bouncycastle.util.io.Streams; import org.junit.jupiter.api.Test; import org.pgpainless.PGPainless; import org.pgpainless.algorithm.DocumentSignatureType; -import org.pgpainless.decryption_verification.cleartext_signatures.CleartextSignatureProcessor; import org.pgpainless.decryption_verification.cleartext_signatures.InMemoryMultiPassStrategy; import org.pgpainless.decryption_verification.cleartext_signatures.MultiPassStrategy; +import org.pgpainless.decryption_verification.cleartext_signatures.VerifyCleartextSignaturesImpl; import org.pgpainless.encryption_signing.EncryptionStream; import org.pgpainless.encryption_signing.ProducerOptions; import org.pgpainless.encryption_signing.SigningOptions; @@ -79,12 +79,11 @@ public class CleartextSignatureVerificationTest { .addVerificationCert(signingKeys); InMemoryMultiPassStrategy multiPassStrategy = MultiPassStrategy.keepMessageInMemory(); - CleartextSignatureProcessor processor = PGPainless.verifyCleartextSignedMessage() + options.setMultiPassStrategy(multiPassStrategy); + DecryptionStream decryptionStream = PGPainless.decryptAndOrVerify() .onInputStream(new ByteArrayInputStream(MESSAGE_SIGNED)) - .withStrategy(multiPassStrategy) .withOptions(options); - DecryptionStream decryptionStream = processor.getVerificationStream(); ByteArrayOutputStream out = new ByteArrayOutputStream(); Streams.pipeAll(decryptionStream, out); decryptionStream.close(); @@ -107,13 +106,11 @@ public class CleartextSignatureVerificationTest { File tempDir = TestUtils.createTempDirectory(); File file = new File(tempDir, "file"); MultiPassStrategy multiPassStrategy = MultiPassStrategy.writeMessageToFile(file); - CleartextSignatureProcessor processor = PGPainless.verifyCleartextSignedMessage() + options.setMultiPassStrategy(multiPassStrategy); + DecryptionStream decryptionStream = PGPainless.decryptAndOrVerify() .onInputStream(new ByteArrayInputStream(MESSAGE_SIGNED)) - .withStrategy(multiPassStrategy) .withOptions(options); - DecryptionStream decryptionStream = processor.getVerificationStream(); - ByteArrayOutputStream out = new ByteArrayOutputStream(); Streams.pipeAll(decryptionStream, out); decryptionStream.close(); @@ -173,18 +170,6 @@ public class CleartextSignatureVerificationTest { assertEquals(1, metadata.getVerifiedSignatures().size()); } - @Test - public void consumingCleartextSignedMessageWithNormalAPIThrowsWrongConsumingMethodException() throws IOException, PGPException { - PGPPublicKeyRing certificate = TestKeys.getEmilPublicKeyRing(); - ConsumerOptions options = new ConsumerOptions() - .addVerificationCert(certificate); - - assertThrows(WrongConsumingMethodException.class, () -> - PGPainless.decryptAndOrVerify() - .onInputStream(new ByteArrayInputStream(MESSAGE_SIGNED)) - .withOptions(options)); - } - @Test public void consumingInlineSignedMessageWithCleartextSignedVerificationApiThrowsWrongConsumingMethodException() throws PGPException, IOException { String inlineSignedMessage = "-----BEGIN PGP MESSAGE-----\n" + @@ -207,11 +192,10 @@ public class CleartextSignatureVerificationTest { .addVerificationCert(certificate); assertThrows(WrongConsumingMethodException.class, () -> - PGPainless.verifyCleartextSignedMessage() - .onInputStream(new ByteArrayInputStream(inlineSignedMessage.getBytes(StandardCharsets.UTF_8))) - .withStrategy(new InMemoryMultiPassStrategy()) - .withOptions(options) - .getVerificationStream()); + new VerifyCleartextSignaturesImpl() + .onInputStream(new ByteArrayInputStream(inlineSignedMessage.getBytes(StandardCharsets.UTF_8))) + .withOptions(options) + .getVerificationStream()); } @Test @@ -223,7 +207,7 @@ public class CleartextSignatureVerificationTest { ByteArrayOutputStream signedOut = new ByteArrayOutputStream(); EncryptionStream signingStream = PGPainless.encryptAndOrSign().onOutputStream(signedOut) .withOptions(ProducerOptions.sign(SigningOptions.get() - .addDetachedSignature(SecretKeyRingProtector.unprotectedKeys(), secretKey, DocumentSignatureType.CANONICAL_TEXT_DOCUMENT)) + .addDetachedSignature(SecretKeyRingProtector.unprotectedKeys(), secretKey, DocumentSignatureType.CANONICAL_TEXT_DOCUMENT)) .setCleartextSigned()); Streams.pipeAll(msgIn, signingStream); @@ -232,12 +216,10 @@ public class CleartextSignatureVerificationTest { String signed = signedOut.toString(); ByteArrayInputStream signedIn = new ByteArrayInputStream(signed.getBytes(StandardCharsets.UTF_8)); - DecryptionStream verificationStream = PGPainless.verifyCleartextSignedMessage() + DecryptionStream verificationStream = PGPainless.decryptAndOrVerify() .onInputStream(signedIn) - .withStrategy(new InMemoryMultiPassStrategy()) .withOptions(new ConsumerOptions() - .addVerificationCert(TestKeys.getEmilPublicKeyRing())) - .getVerificationStream(); + .addVerificationCert(TestKeys.getEmilPublicKeyRing())); ByteArrayOutputStream msgOut = new ByteArrayOutputStream(); Streams.pipeAll(verificationStream, msgOut); 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 da641325..d0f3461c 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/example/DecryptOrVerify.java +++ b/pgpainless-core/src/test/java/org/pgpainless/example/DecryptOrVerify.java @@ -7,7 +7,6 @@ package org.pgpainless.example; import static org.junit.jupiter.api.Assertions.assertArrayEquals; import static org.junit.jupiter.api.Assertions.assertTrue; -import java.io.BufferedInputStream; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; @@ -24,11 +23,9 @@ 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.cleartext_signatures.InMemoryMultiPassStrategy; import org.pgpainless.encryption_signing.EncryptionStream; import org.pgpainless.encryption_signing.ProducerOptions; import org.pgpainless.encryption_signing.SigningOptions; -import org.pgpainless.exception.WrongConsumingMethodException; import org.pgpainless.key.protection.SecretKeyRingProtector; public class DecryptOrVerify { @@ -97,22 +94,10 @@ public class DecryptOrVerify { for (String signed : new String[] {INBAND_SIGNED, CLEARTEXT_SIGNED}) { ByteArrayOutputStream out = new ByteArrayOutputStream(); ByteArrayInputStream in = new ByteArrayInputStream(signed.getBytes(StandardCharsets.UTF_8)); - BufferedInputStream bufIn = new BufferedInputStream(in); - bufIn.mark(512); DecryptionStream verificationStream; - try { verificationStream = PGPainless.decryptAndOrVerify() - .onInputStream(bufIn) + .onInputStream(in) .withOptions(options); - } catch (WrongConsumingMethodException e) { - bufIn.reset(); - // Cleartext Signed Message - verificationStream = PGPainless.verifyCleartextSignedMessage() - .onInputStream(bufIn) - .withStrategy(new InMemoryMultiPassStrategy()) - .withOptions(options) - .getVerificationStream(); - } Streams.pipeAll(verificationStream, out); verificationStream.close(); @@ -140,11 +125,9 @@ public class DecryptOrVerify { ByteArrayInputStream signedIn = new ByteArrayInputStream(out.toByteArray()); - DecryptionStream verificationStream = PGPainless.verifyCleartextSignedMessage() + DecryptionStream verificationStream = PGPainless.decryptAndOrVerify() .onInputStream(signedIn) - .withStrategy(new InMemoryMultiPassStrategy()) - .withOptions(new ConsumerOptions().addVerificationCert(certificate)) - .getVerificationStream(); + .withOptions(new ConsumerOptions().addVerificationCert(certificate)); ByteArrayOutputStream plain = new ByteArrayOutputStream(); Streams.pipeAll(verificationStream, plain);