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 66b5c4c8..c23ede3d 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,7 +53,6 @@ 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. @@ -346,21 +345,4 @@ public class ConsumerOptions { public MultiPassStrategy getMultiPassStrategy() { return multiPassStrategy; } - - /** - * INTERNAL method to mark cleartext signed messages. - * Do not call this manually. - */ - public ConsumerOptions setIsCleartextSigned() { - this.cleartextSigned = true; - return this; - } - - /** - * 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/DecryptionBuilder.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionBuilder.java index 6cc28a7f..68a68847 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,19 +4,14 @@ 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 { - public static int BUFFER_SIZE = 4096; - @Override public DecryptWith onInputStream(@Nonnull InputStream inputStream) { return new DecryptWithImpl(inputStream); @@ -24,11 +19,10 @@ public class DecryptionBuilder implements DecryptionBuilderInterface { static class DecryptWithImpl implements DecryptWith { - private final BufferedInputStream inputStream; + private final InputStream inputStream; DecryptWithImpl(InputStream inputStream) { - this.inputStream = new BufferedInputStream(inputStream, BUFFER_SIZE); - this.inputStream.mark(BUFFER_SIZE); + this.inputStream = inputStream; } @Override @@ -37,15 +31,7 @@ public class DecryptionBuilder implements DecryptionBuilderInterface { throw new IllegalArgumentException("Consumer options cannot be null."); } - try { - return DecryptionStreamFactory.create(inputStream, consumerOptions); - } catch (WrongConsumingMethodException e) { - inputStream.reset(); - return new VerifyCleartextSignaturesImpl() - .onInputStream(inputStream) - .withOptions(consumerOptions) - .getVerificationStream(); - } + return DecryptionStreamFactory.create(inputStream, consumerOptions); } } } 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 575a5e5f..fbdd6229 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,6 +36,7 @@ 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.PGPUtil; import org.bouncycastle.openpgp.operator.PBEDataDecryptorFactory; import org.bouncycastle.openpgp.operator.PGPContentVerifierBuilderProvider; @@ -46,6 +47,8 @@ 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; @@ -53,7 +56,6 @@ import org.pgpainless.exception.MissingLiteralDataException; import org.pgpainless.exception.MissingPassphraseException; import org.pgpainless.exception.SignatureValidationException; import org.pgpainless.exception.UnacceptableAlgorithmException; -import org.pgpainless.exception.WrongConsumingMethodException; import org.pgpainless.implementation.ImplementationFactory; import org.pgpainless.key.SubkeyIdentifier; import org.pgpainless.key.info.KeyRingInfo; @@ -62,6 +64,7 @@ import org.pgpainless.key.protection.UnlockSecretKey; import org.pgpainless.signature.SignatureUtils; import org.pgpainless.signature.consumer.DetachedSignatureCheck; import org.pgpainless.signature.consumer.OnePassSignatureCheck; +import org.pgpainless.util.ArmoredInputStreamFactory; import org.pgpainless.util.CRCingArmoredInputStreamWrapper; import org.pgpainless.util.PGPUtilWrapper; import org.pgpainless.util.Passphrase; @@ -77,6 +80,9 @@ public final class DecryptionStreamFactory { // Maximum nesting depth of packets (e.g. compression, encryption...) private static final int MAX_PACKET_NESTING_DEPTH = 16; + // Buffer Size for BufferedInputStreams + public static int BUFFER_SIZE = 4096; + private final ConsumerOptions options; private final OpenPgpMetadata.Builder resultBuilder = OpenPgpMetadata.getBuilder(); private final List onePassSignatureChecks = new ArrayList<>(); @@ -92,7 +98,8 @@ public final class DecryptionStreamFactory { @Nonnull ConsumerOptions options) throws PGPException, IOException { DecryptionStreamFactory factory = new DecryptionStreamFactory(options); - return factory.parseOpenPGPDataAndCreateDecryptionStream(inputStream); + BufferedInputStream bufferedIn = new BufferedInputStream(inputStream, BUFFER_SIZE); + return factory.parseOpenPGPDataAndCreateDecryptionStream(bufferedIn); } public DecryptionStreamFactory(ConsumerOptions options) { @@ -125,44 +132,34 @@ public final class DecryptionStreamFactory { } } - private DecryptionStream parseOpenPGPDataAndCreateDecryptionStream(InputStream inputStream) + private DecryptionStream parseOpenPGPDataAndCreateDecryptionStream(BufferedInputStream bufferedIn) throws IOException, PGPException { - // Make sure we handle armored and non-armored data properly - BufferedInputStream bufferedIn = new BufferedInputStream(inputStream, 512); - bufferedIn.mark(512); - InputStream decoderStream; + InputStream pgpInStream; + InputStream outerDecodingStream; PGPObjectFactory objectFactory; - // Workaround for cleartext signed data - // If we below threw a WrongConsumingMethodException, the CleartextSignatureProcessor will prepare the - // message for us and will set options.isCleartextSigned() to true. - // That way we can process long messages without running the issue of resetting the bufferedInputStream - // to invalid marks. - if (options.isCleartextSigned()) { - inputStream = wrapInVerifySignatureStream(bufferedIn, null); - return new DecryptionStream(inputStream, resultBuilder, integrityProtectedEncryptedInputStream, - null); - } - try { - decoderStream = PGPUtilWrapper.getDecoderStream(bufferedIn); - decoderStream = CRCingArmoredInputStreamWrapper.possiblyWrap(decoderStream); + outerDecodingStream = PGPUtilWrapper.getDecoderStream(bufferedIn); + outerDecodingStream = CRCingArmoredInputStreamWrapper.possiblyWrap(outerDecodingStream); - if (decoderStream instanceof ArmoredInputStream) { - ArmoredInputStream armor = (ArmoredInputStream) decoderStream; + if (outerDecodingStream instanceof ArmoredInputStream) { + ArmoredInputStream armor = (ArmoredInputStream) outerDecodingStream; // Cleartext Signed Message // Throw a WrongConsumingMethodException to delegate preparation (extraction of signatures) // to the CleartextSignatureProcessor which will call us again (see comment above) if (armor.isClearText()) { - throw new WrongConsumingMethodException("Message appears to be using the Cleartext Signature Framework. " + - "Use PGPainless.verifyCleartextSignedMessage() to verify this message instead."); + bufferedIn.reset(); + return parseCleartextSignedMessage(bufferedIn); } } - objectFactory = ImplementationFactory.getInstance().getPGPObjectFactory(decoderStream); + objectFactory = ImplementationFactory.getInstance().getPGPObjectFactory(outerDecodingStream); // Parse OpenPGP message - inputStream = processPGPPackets(objectFactory, 1); + pgpInStream = processPGPPackets(objectFactory, 1); + return new DecryptionStream(pgpInStream, + resultBuilder, integrityProtectedEncryptedInputStream, + (outerDecodingStream instanceof ArmoredInputStream) ? outerDecodingStream : null); } catch (EOFException | FinalIOException e) { // Broken message or invalid decryption session key throw e; @@ -172,23 +169,44 @@ public final class DecryptionStreamFactory { // to allow for detached signature verification. LOGGER.debug("The message appears to not be an OpenPGP message. This is probably data signed with detached signatures?"); bufferedIn.reset(); - decoderStream = bufferedIn; - objectFactory = ImplementationFactory.getInstance().getPGPObjectFactory(decoderStream); - inputStream = wrapInVerifySignatureStream(bufferedIn, objectFactory); + outerDecodingStream = bufferedIn; + objectFactory = ImplementationFactory.getInstance().getPGPObjectFactory(outerDecodingStream); + pgpInStream = wrapInVerifySignatureStream(bufferedIn, objectFactory); } catch (IOException e) { if (e.getMessage().contains("invalid armor") || e.getMessage().contains("invalid header encountered")) { // We falsely assumed the data to be armored. LOGGER.debug("The message is apparently not armored."); bufferedIn.reset(); - decoderStream = bufferedIn; - inputStream = wrapInVerifySignatureStream(bufferedIn, null); + outerDecodingStream = CRCingArmoredInputStreamWrapper.possiblyWrap(bufferedIn); + pgpInStream = wrapInVerifySignatureStream(outerDecodingStream, null); } else { throw new FinalIOException(e); } } - return new DecryptionStream(inputStream, resultBuilder, integrityProtectedEncryptedInputStream, - (decoderStream instanceof ArmoredInputStream) ? decoderStream : null); + return new DecryptionStream(pgpInStream, resultBuilder, integrityProtectedEncryptedInputStream, + (outerDecodingStream instanceof ArmoredInputStream) ? outerDecodingStream : null); + } + + private DecryptionStream parseCleartextSignedMessage(BufferedInputStream in) + throws IOException, PGPException { + resultBuilder.setCompressionAlgorithm(CompressionAlgorithm.UNCOMPRESSED) + .setFileEncoding(StreamEncoding.TEXT); + + ArmoredInputStream armorIn = ArmoredInputStreamFactory.get(in); + + 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 DecryptionStream(verifyIn, resultBuilder, integrityProtectedEncryptedInputStream, + null); } private InputStream wrapInVerifySignatureStream(InputStream bufferedIn, @Nullable 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 deleted file mode 100644 index 26e33a96..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/cleartext_signatures/CleartextSignatureProcessor.java +++ /dev/null @@ -1,75 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.decryption_verification.cleartext_signatures; - -import java.io.IOException; -import java.io.InputStream; - -import org.bouncycastle.bcpg.ArmoredInputStream; -import org.bouncycastle.openpgp.PGPException; -import org.bouncycastle.openpgp.PGPSignature; -import org.bouncycastle.openpgp.PGPSignatureList; -import org.pgpainless.PGPainless; -import org.pgpainless.algorithm.CompressionAlgorithm; -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.exception.SignatureValidationException; -import org.pgpainless.util.ArmoredInputStreamFactory; - -/** - * Processor for cleartext-signed messages. - */ -public class CleartextSignatureProcessor { - - private final ArmoredInputStream in; - private final ConsumerOptions options; - - public CleartextSignatureProcessor(InputStream inputStream, - ConsumerOptions options) - throws IOException { - if (inputStream instanceof ArmoredInputStream) { - this.in = (ArmoredInputStream) inputStream; - } else { - this.in = ArmoredInputStreamFactory.get(inputStream); - } - this.options = options; - } - - /** - * Perform the first pass of cleartext signed message processing: - * Unpack the message from the ascii armor and detach signatures. - * The plaintext message is being written to cache/disk according to the used {@link MultiPassStrategy}. - * - * The result of this method is a {@link DecryptionStream} which will perform the second pass. - * It again outputs the plaintext message and performs signature verification. - * - * The result of {@link DecryptionStream#getResult()} contains information about the messages signatures. - * - * @return validated signature - * @throws IOException if the signature cannot be read. - * @throws PGPException if the signature cannot be initialized. - * @throws SignatureValidationException if the signature is invalid. - */ - public DecryptionStream getVerificationStream() throws IOException, PGPException { - OpenPgpMetadata.Builder resultBuilder = OpenPgpMetadata.getBuilder(); - resultBuilder.setCompressionAlgorithm(CompressionAlgorithm.UNCOMPRESSED) - .setFileEncoding(StreamEncoding.TEXT); - - MultiPassStrategy multiPassStrategy = options.getMultiPassStrategy(); - PGPSignatureList signatures = ClearsignedMessageUtil.detachSignaturesFromInbandClearsignedMessage(in, multiPassStrategy.getMessageOutputStream()); - - for (PGPSignature signature : signatures) { - options.addVerificationOfDetachedSignature(signature); - } - - options.setIsCleartextSigned(); - return PGPainless.decryptAndOrVerify() - .onInputStream(multiPassStrategy.getMessageInputStream()) - .withOptions(options); - } - -} diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/cleartext_signatures/MultiPassStrategy.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/cleartext_signatures/MultiPassStrategy.java index 688105a1..5aa9f548 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/cleartext_signatures/MultiPassStrategy.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/cleartext_signatures/MultiPassStrategy.java @@ -11,7 +11,7 @@ import java.io.InputStream; import java.io.OutputStream; /** - * Since the {@link CleartextSignatureProcessor} needs to read the whole data twice in order to verify signatures, + * Since for verification of cleartext signed messages, we need to read the whole data twice in order to verify signatures, * a strategy for how to cache the read data is required. * Otherwise, large data kept in memory could cause {@link OutOfMemoryError OutOfMemoryErrors} or other issues. * 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 deleted file mode 100644 index 52360869..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/cleartext_signatures/VerifyCleartextSignatures.java +++ /dev/null @@ -1,36 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.decryption_verification.cleartext_signatures; - -import java.io.IOException; -import java.io.InputStream; - -import org.pgpainless.decryption_verification.ConsumerOptions; - -/** - * Interface defining the API for verification of cleartext signed documents. - */ -public interface VerifyCleartextSignatures { - - /** - * Provide the {@link InputStream} which contains the cleartext-signed message. - * @param inputStream inputstream - * @return api handle - */ - VerifyWith onInputStream(InputStream inputStream); - - interface VerifyWith { - - /** - * Pass in consumer options like verification certificates, acceptable date ranges etc. - * - * @param options options - * @return processor - * @throws IOException in case of an IO error - */ - CleartextSignatureProcessor withOptions(ConsumerOptions options) throws IOException; - - } -} 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 deleted file mode 100644 index fde90874..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/cleartext_signatures/VerifyCleartextSignaturesImpl.java +++ /dev/null @@ -1,30 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.decryption_verification.cleartext_signatures; - -import java.io.IOException; -import java.io.InputStream; - -import org.pgpainless.decryption_verification.ConsumerOptions; - -public class VerifyCleartextSignaturesImpl implements VerifyCleartextSignatures { - - private InputStream inputStream; - - @Override - public VerifyWithImpl onInputStream(InputStream inputStream) { - VerifyCleartextSignaturesImpl.this.inputStream = inputStream; - return new VerifyWithImpl(); - } - - public class VerifyWithImpl implements VerifyWith { - - @Override - public CleartextSignatureProcessor withOptions(ConsumerOptions options) throws IOException { - 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 4a2a99f5..bcf1321e 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 @@ -6,7 +6,6 @@ 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.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import java.io.ByteArrayInputStream; @@ -30,11 +29,9 @@ import org.pgpainless.PGPainless; import org.pgpainless.algorithm.DocumentSignatureType; 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; -import org.pgpainless.exception.WrongConsumingMethodException; import org.pgpainless.key.TestKeys; import org.pgpainless.key.protection.SecretKeyRingProtector; import org.pgpainless.signature.consumer.CertificateValidator; @@ -180,35 +177,6 @@ public class CleartextSignatureVerificationTest { assertEquals(1, metadata.getVerifiedSignatures().size()); } - @Test - public void consumingInlineSignedMessageWithCleartextSignedVerificationApiThrowsWrongConsumingMethodException() - throws IOException { - String inlineSignedMessage = "-----BEGIN PGP MESSAGE-----\n" + - "Version: PGPainless\n" + - "\n" + - "kA0DAQoTVzbmkxrPNwwBy8BJYgAAAAAAQWgsIEp1bGlldCwgaWYgdGhlIG1lYXN1\n" + - "cmUgb2YgdGh5IGpveQpCZSBoZWFwZWQgbGlrZSBtaW5lLCBhbmQgdGhhdCB0aHkg\n" + - "c2tpbGwgYmUgbW9yZQpUbyBibGF6b24gaXQsIHRoZW4gc3dlZXRlbiB3aXRoIHRo\n" + - "eSBicmVhdGgKVGhpcyBuZWlnaGJvciBhaXIsIGFuZCBsZXQgcmljaCBtdXNpY+KA\n" + - "mXMgdG9uZ3VlClVuZm9sZCB0aGUgaW1hZ2luZWQgaGFwcGluZXNzIHRoYXQgYm90\n" + - "aApSZWNlaXZlIGluIGVpdGhlciBieSB0aGlzIGRlYXIgZW5jb3VudGVyLoh1BAET\n" + - "CgAGBQJhK2q9ACEJEFc25pMazzcMFiEET2ZcTcLEZgvGQl5BVzbmkxrPNwxr8gD+\n" + - "MDfg+qccpsoJVgHIW8mRPBQowXDyw+oNHsf28ii+/pEBAO/RXhFkZBPzlfDJMJVT\n" + - "UwJJeuna1R4yOoWjq0zqRvrg\n" + - "=dBiV\n" + - "-----END PGP MESSAGE-----\n"; - - PGPPublicKeyRing certificate = TestKeys.getEmilPublicKeyRing(); - ConsumerOptions options = new ConsumerOptions() - .addVerificationCert(certificate); - - assertThrows(WrongConsumingMethodException.class, () -> - new VerifyCleartextSignaturesImpl() - .onInputStream(new ByteArrayInputStream(inlineSignedMessage.getBytes(StandardCharsets.UTF_8))) - .withOptions(options) - .getVerificationStream()); - } - @Test public void getDecoderStreamMistakensPlaintextForBase64RegressionTest() throws PGPException, IOException {