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 6d7cb069..bcc9099e 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 @@ -46,6 +46,7 @@ import org.pgpainless.algorithm.CompressionAlgorithm; import org.pgpainless.algorithm.EncryptionPurpose; import org.pgpainless.algorithm.StreamEncoding; import org.pgpainless.algorithm.SymmetricKeyAlgorithm; +import org.pgpainless.exception.FinalIOException; import org.pgpainless.exception.MessageNotIntegrityProtectedException; import org.pgpainless.exception.MissingDecryptionMethodException; import org.pgpainless.exception.MissingLiteralDataException; @@ -154,7 +155,7 @@ public final class DecryptionStreamFactory { objectFactory = ImplementationFactory.getInstance().getPGPObjectFactory(decoderStream); // Parse OpenPGP message inputStream = processPGPPackets(objectFactory, 1); - } catch (EOFException e) { + } catch (EOFException | FinalIOException e) { throw e; } catch (MissingLiteralDataException e) { // Not an OpenPGP message. @@ -174,7 +175,7 @@ public final class DecryptionStreamFactory { objectFactory = ImplementationFactory.getInstance().getPGPObjectFactory(decoderStream); inputStream = wrapInVerifySignatureStream(bufferedIn, objectFactory); } else { - throw e; + throw new FinalIOException(e); } } @@ -195,18 +196,28 @@ public final class DecryptionStreamFactory { throw new PGPException("Maximum depth of nested packages exceeded."); } Object nextPgpObject; - while ((nextPgpObject = objectFactory.nextObject()) != null) { - if (nextPgpObject instanceof PGPEncryptedDataList) { - return processPGPEncryptedDataList((PGPEncryptedDataList) nextPgpObject, depth); + 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); + } } - 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("unknown object in stream:")) { + throw new MissingLiteralDataException("No Literal Data Packet found."); + } else { + throw new FinalIOException(e); } } diff --git a/pgpainless-core/src/main/java/org/pgpainless/exception/FinalIOException.java b/pgpainless-core/src/main/java/org/pgpainless/exception/FinalIOException.java new file mode 100644 index 00000000..6b6f86de --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/exception/FinalIOException.java @@ -0,0 +1,17 @@ +// 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/test/java/org/pgpainless/decryption_verification/VerifyDetachedSignatureTest.java b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/VerifyDetachedSignatureTest.java new file mode 100644 index 00000000..01bc547d --- /dev/null +++ b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/VerifyDetachedSignatureTest.java @@ -0,0 +1,71 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.decryption_verification; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; + +import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.util.io.Streams; +import org.junit.jupiter.api.Test; +import org.pgpainless.PGPainless; +import org.pgpainless.decryption_verification.cleartext_signatures.InMemoryMultiPassStrategy; + +public class VerifyDetachedSignatureTest { + + @Test + public void verify() throws PGPException, IOException { + String signedContent = "Content-Type: multipart/mixed; boundary=\"OSR6TONWKJD9dgyc2XH5AQPNnAs7pdg1t\"\n" + + "\n" + + "--OSR6TONWKJD9dgyc2XH5AQPNnAs7pdg1t\n" + + "Content-Type: text/plain; charset=utf-8\n" + + "Content-Transfer-Encoding: quoted-printable\n" + + "Content-Language: en-US\n" + + "\n" + + "NOT encrypted + signed(detached)\n" + + "\n" + + "\n" + + "\n" + + "--OSR6TONWKJD9dgyc2XH5AQPNnAs7pdg1t--\n"; + String signature = "-----BEGIN PGP SIGNATURE-----\n" + + "\n" + + "iHUEARYIAB0WIQTBZCjWAcs5N4nPYdTDIInNavjWzgUCYgKPzAAKCRDDIInNavjW\n" + + "zmdoAP0TdFt1OWqosHhXxt2hNYqZQMc6bgQRpJNL029nRyzkPAD/SoYJ4T+aYEhw\n" + + "11qrbXloqkr0G3QaA6/zk31RPMI/bgI=\n" + + "=o5Ze\n" + + "-----END PGP SIGNATURE-----\n"; + String pubkey = "-----BEGIN PGP PUBLIC KEY BLOCK-----\n" + + "Version: PGPainless\n" + + "\n" + + "mDMEYIucWBYJKwYBBAHaRw8BAQdAew+8mzMWyf3+Pfy49qa60uKV6e5os7de4TdZ\n" + + "ceAWUq+0F2RlbmJvbmQ3QGZsb3djcnlwdC50ZXN0iHgEExYKACAFAmCLnFgCGwMF\n" + + "FgIDAQAECwkIBwUVCgkICwIeAQIZAQAKCRDDIInNavjWzm3JAQCgFgCEyD58iEa/\n" + + "Rw/DYNoQNoZC1lhw1bxBiOcIbtkdBgEAsDFZu3TBavOMKI7KW+vfMBHtRVbkMNpv\n" + + "unaAldoabgO4OARgi5xYEgorBgEEAZdVAQUBAQdAB1/Mrq5JGYim4KqGTSK4OESQ\n" + + "UwPgK56q0yrkiU9WgyYDAQgHiHUEGBYKAB0FAmCLnFgCGwwFFgIDAQAECwkIBwUV\n" + + "CgkICwIeAQAKCRDDIInNavjWzjMgAQCU+R1fItqdY6lt9jXUqipmXuqVaEFPwNA8\n" + + "YJ1rIwDwVQEAyUc8162KWzA2iQB5akwLwNr/pLDDtOWwhLUkrBb3mAc=\n" + + "=pXF6\n" + + "-----END PGP PUBLIC KEY BLOCK-----\n"; + + + DecryptionStream verifier = PGPainless.decryptAndOrVerify() + .onInputStream(new ByteArrayInputStream(signedContent.getBytes(StandardCharsets.UTF_8))) + .withOptions( + new ConsumerOptions() + .addVerificationOfDetachedSignatures(new ByteArrayInputStream(signature.getBytes(StandardCharsets.UTF_8))) + .addVerificationCerts(PGPainless.readKeyRing().keyRingCollection(pubkey, true).getPgpPublicKeyRingCollection()) + .setMultiPassStrategy(new InMemoryMultiPassStrategy()) + ); + + Streams.drain(verifier); + verifier.close(); + OpenPgpMetadata metadata = verifier.getResult(); + assertTrue(metadata.isVerified()); + } +}