From 5c93eb3705c551edc9d05bfa2a1b159084a66dd5 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 26 Sep 2022 18:21:06 +0200 Subject: [PATCH] Wip: Introduce MessageMetadata class --- .../MessageMetadata.java | 249 ++++++++++++++++++ .../OpenPgpMessageInputStream.java | 108 ++++---- .../automaton/PDA.java | 3 - .../MessageMetadataTest.java | 84 ++++++ .../OpenPgpMessageInputStreamTest.java | 182 +++++++------ 5 files changed, 498 insertions(+), 128 deletions(-) create mode 100644 pgpainless-core/src/main/java/org/pgpainless/decryption_verification/MessageMetadata.java create mode 100644 pgpainless-core/src/test/java/org/pgpainless/decryption_verification/MessageMetadataTest.java diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/MessageMetadata.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/MessageMetadata.java new file mode 100644 index 00000000..7c09fc8d --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/MessageMetadata.java @@ -0,0 +1,249 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.decryption_verification; + +import org.pgpainless.algorithm.CompressionAlgorithm; +import org.pgpainless.algorithm.StreamEncoding; +import org.pgpainless.algorithm.SymmetricKeyAlgorithm; +import org.pgpainless.util.SessionKey; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.util.ArrayList; +import java.util.Date; +import java.util.Iterator; +import java.util.List; +import java.util.NoSuchElementException; + +public class MessageMetadata { + + protected Message message; + + public MessageMetadata(@Nonnull Message message) { + this.message = message; + } + + public @Nullable SymmetricKeyAlgorithm getEncryptionAlgorithm() { + Iterator algorithms = getEncryptionAlgorithms(); + if (algorithms.hasNext()) { + return algorithms.next(); + } + return null; + } + + public @Nonnull Iterator getEncryptionAlgorithms() { + return new LayerIterator(message) { + @Override + public boolean matches(Nested layer) { + return layer instanceof EncryptedData; + } + + @Override + public SymmetricKeyAlgorithm getProperty(Layer last) { + return ((EncryptedData) last).algorithm; + } + }; + } + + public @Nullable CompressionAlgorithm getCompressionAlgorithm() { + Iterator algorithms = getCompressionAlgorithms(); + if (algorithms.hasNext()) { + return algorithms.next(); + } + return null; + } + + public @Nonnull Iterator getCompressionAlgorithms() { + return new LayerIterator(message) { + @Override + public boolean matches(Nested layer) { + return layer instanceof CompressedData; + } + + @Override + public CompressionAlgorithm getProperty(Layer last) { + return ((CompressedData) last).algorithm; + } + }; + } + + public String getFilename() { + return findLiteralData().getFileName(); + } + + public Date getModificationDate() { + return findLiteralData().getModificationDate(); + } + + public StreamEncoding getFormat() { + return findLiteralData().getFormat(); + } + + private LiteralData findLiteralData() { + Nested nested = message.child; + while (nested.hasNestedChild()) { + Layer layer = (Layer) nested; + nested = layer.child; + } + return (LiteralData) nested; + } + + public static abstract class Layer { + protected final List verifiedSignatures = new ArrayList<>(); + protected final List failedSignatures = new ArrayList<>(); + protected Nested child; + + public Nested getChild() { + return child; + } + + public void setChild(Nested child) { + this.child = child; + } + + public List getVerifiedSignatures() { + return new ArrayList<>(verifiedSignatures); + } + + public List getFailedSignatures() { + return new ArrayList<>(failedSignatures); + } + } + + public interface Nested { + boolean hasNestedChild(); + } + + public static class Message extends Layer { + + } + + public static class LiteralData implements Nested { + protected String fileName; + protected Date modificationDate; + protected StreamEncoding format; + + public LiteralData() { + this("", new Date(0L), StreamEncoding.BINARY); + } + + public LiteralData(String fileName, Date modificationDate, StreamEncoding format) { + this.fileName = fileName; + this.modificationDate = modificationDate; + this.format = format; + } + + public String getFileName() { + return fileName; + } + + public Date getModificationDate() { + return modificationDate; + } + + public StreamEncoding getFormat() { + return format; + } + + @Override + public boolean hasNestedChild() { + return false; + } + } + + public static class CompressedData extends Layer implements Nested { + protected final CompressionAlgorithm algorithm; + + public CompressedData(CompressionAlgorithm zip) { + this.algorithm = zip; + } + + public CompressionAlgorithm getAlgorithm() { + return algorithm; + } + + @Override + public boolean hasNestedChild() { + return true; + } + } + + public static class EncryptedData extends Layer implements Nested { + protected final SymmetricKeyAlgorithm algorithm; + protected SessionKey sessionKey; + protected List recipients; + + public EncryptedData(SymmetricKeyAlgorithm algorithm) { + this.algorithm = algorithm; + } + + public SymmetricKeyAlgorithm getAlgorithm() { + return algorithm; + } + + public SessionKey getSessionKey() { + return sessionKey; + } + + public List getRecipients() { + return new ArrayList<>(recipients); + } + + @Override + public boolean hasNestedChild() { + return true; + } + } + + + private static abstract class LayerIterator implements Iterator { + private Nested current; + Layer last = null; + + public LayerIterator(Message message) { + super(); + this.current = message.child; + if (matches(current)) { + last = (Layer) current; + } + } + + @Override + public boolean hasNext() { + if (last == null) { + findNext(); + } + return last != null; + } + + @Override + public O next() { + if (last == null) { + findNext(); + } + if (last != null) { + O property = getProperty(last); + last = null; + return property; + } + throw new NoSuchElementException(); + } + + private void findNext() { + while (current instanceof Layer) { + current = ((Layer) current).child; + if (matches(current)) { + last = (Layer) current; + break; + } + } + } + + abstract boolean matches(Nested layer); + + abstract O getProperty(Layer last); + } + +} diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java index 990e628d..44ebb27e 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java @@ -31,11 +31,11 @@ import org.bouncycastle.openpgp.operator.PBEDataDecryptorFactory; import org.bouncycastle.openpgp.operator.PGPContentVerifierBuilderProvider; import org.bouncycastle.openpgp.operator.PublicKeyDataDecryptorFactory; import org.bouncycastle.openpgp.operator.SessionKeyDataDecryptorFactory; -import org.bouncycastle.pqc.crypto.rainbow.Layer; import org.pgpainless.PGPainless; import org.pgpainless.algorithm.CompressionAlgorithm; import org.pgpainless.algorithm.EncryptionPurpose; import org.pgpainless.algorithm.OpenPgpPacket; +import org.pgpainless.algorithm.StreamEncoding; import org.pgpainless.algorithm.SymmetricKeyAlgorithm; import org.pgpainless.decryption_verification.automaton.InputAlphabet; import org.pgpainless.decryption_verification.automaton.PDA; @@ -69,14 +69,14 @@ public class OpenPgpMessageInputStream extends InputStream { private boolean closed = false; private Signatures signatures; - private LayerMetadata layerMetadata; + private MessageMetadata.Layer metadata; public OpenPgpMessageInputStream(InputStream inputStream, ConsumerOptions options) throws IOException, PGPException { - this(inputStream, options, null); + this(inputStream, options, new MessageMetadata.Message()); } - OpenPgpMessageInputStream(InputStream inputStream, ConsumerOptions options, LayerMetadata layerMetadata) + OpenPgpMessageInputStream(InputStream inputStream, ConsumerOptions options, MessageMetadata.Layer metadata) throws PGPException, IOException { // TODO: Use BCPGInputStream.wrap(inputStream); if (inputStream instanceof BCPGInputStream) { @@ -86,33 +86,12 @@ public class OpenPgpMessageInputStream extends InputStream { } this.options = options; + this.metadata = metadata; this.resultBuilder = OpenPgpMetadata.getBuilder(); this.signatures = new Signatures(options); this.signatures.addDetachedSignatures(options.getDetachedSignatures()); - consumePackets(); - } - - static class LayerMetadata { - - private CompressionAlgorithm compressionAlgorithm; - private SymmetricKeyAlgorithm symmetricKeyAlgorithm; - private LayerMetadata child; - - public LayerMetadata setCompressionAlgorithm(CompressionAlgorithm algorithm) { - this.compressionAlgorithm = algorithm; - return this; - } - - public LayerMetadata setSymmetricEncryptionAlgorithm(SymmetricKeyAlgorithm algorithm) { - this.symmetricKeyAlgorithm = algorithm; - return this; - } - - public LayerMetadata setChild(LayerMetadata child) { - this.child = child; - return this; - } + consumePackets(); // nom nom nom } /** @@ -127,27 +106,21 @@ public class OpenPgpMessageInputStream extends InputStream { */ private void consumePackets() throws IOException, PGPException { - System.out.println("Walk " + automaton); int tag; loop: while ((tag = nextTag()) != -1) { OpenPgpPacket nextPacket = OpenPgpPacket.requireFromTag(tag); - System.out.println(nextPacket); switch (nextPacket) { // Literal Data - the literal data content is the new input stream case LIT: automaton.next(InputAlphabet.LiteralData); - PGPLiteralData literalData = new PGPLiteralData(bcpgIn); - in = literalData.getDataStream(); + processLiteralData(); break loop; // Compressed Data - the content contains another OpenPGP message case COMP: automaton.next(InputAlphabet.CompressedData); - PGPCompressedData compressedData = new PGPCompressedData(bcpgIn); - LayerMetadata compressionLayer = new LayerMetadata(); - compressionLayer.setCompressionAlgorithm(CompressionAlgorithm.fromId(compressedData.getAlgorithm())); - in = new OpenPgpMessageInputStream(compressedData.getDataStream(), options, compressionLayer); + processCompressedData(); break loop; // One Pass Signatures @@ -160,12 +133,7 @@ public class OpenPgpMessageInputStream extends InputStream { case SIG: boolean isSigForOPS = automaton.peekStack() == StackAlphabet.ops; automaton.next(InputAlphabet.Signatures); - PGPSignatureList signatureList = readSignatures(); - if (isSigForOPS) { - signatures.addOnePassCorrespondingSignatures(signatureList); - } else { - signatures.addPrependedSignatures(signatureList); - } + processSignature(isSigForOPS); break; // Encrypted Data (ESKs and SED/SEIPD are parsed the same by BC) @@ -210,6 +178,29 @@ public class OpenPgpMessageInputStream extends InputStream { } } + private void processSignature(boolean isSigForOPS) throws IOException { + PGPSignatureList signatureList = readSignatures(); + if (isSigForOPS) { + signatures.addOnePassCorrespondingSignatures(signatureList); + } else { + signatures.addPrependedSignatures(signatureList); + } + } + + private void processCompressedData() throws IOException, PGPException { + PGPCompressedData compressedData = new PGPCompressedData(bcpgIn); + MessageMetadata.CompressedData compressionLayer = new MessageMetadata.CompressedData( + CompressionAlgorithm.fromId(compressedData.getAlgorithm())); + in = new OpenPgpMessageInputStream(compressedData.getDataStream(), options, compressionLayer); + } + + private void processLiteralData() throws IOException { + PGPLiteralData literalData = new PGPLiteralData(bcpgIn); + this.metadata.setChild(new MessageMetadata.LiteralData(literalData.getFileName(), literalData.getModificationTime(), + StreamEncoding.requireFromCode(literalData.getFormat()))); + in = literalData.getDataStream(); + } + private boolean processEncryptedData() throws IOException, PGPException { PGPEncryptedDataList encDataList = new PGPEncryptedDataList(bcpgIn); @@ -227,13 +218,14 @@ public class OpenPgpMessageInputStream extends InputStream { // TODO: Replace with encDataList.addSessionKeyDecryptionMethod(sessionKey) PGPEncryptedData esk = esks.all().get(0); try { + MessageMetadata.EncryptedData encryptedData = new MessageMetadata.EncryptedData(options.getSessionKey().getAlgorithm()); if (esk instanceof PGPPBEEncryptedData) { PGPPBEEncryptedData skesk = (PGPPBEEncryptedData) esk; - in = skesk.getDataStream(decryptorFactory); + in = new OpenPgpMessageInputStream(skesk.getDataStream(decryptorFactory), options, encryptedData); return true; } else if (esk instanceof PGPPublicKeyEncryptedData) { PGPPublicKeyEncryptedData pkesk = (PGPPublicKeyEncryptedData) esk; - in = pkesk.getDataStream(decryptorFactory); + in = new OpenPgpMessageInputStream(pkesk.getDataStream(decryptorFactory), options, encryptedData); return true; } else { throw new RuntimeException("Unknown ESK class type: " + esk.getClass().getName()); @@ -250,7 +242,9 @@ public class OpenPgpMessageInputStream extends InputStream { .getPBEDataDecryptorFactory(passphrase); try { InputStream decrypted = skesk.getDataStream(decryptorFactory); - in = new OpenPgpMessageInputStream(decrypted, options); + MessageMetadata.EncryptedData encryptedData = new MessageMetadata.EncryptedData( + SymmetricKeyAlgorithm.requireFromId(skesk.getSymmetricAlgorithm(decryptorFactory))); + in = new OpenPgpMessageInputStream(decrypted, options, encryptedData); return true; } catch (PGPException e) { // password mismatch? Try next password @@ -274,7 +268,9 @@ public class OpenPgpMessageInputStream extends InputStream { .getPublicKeyDataDecryptorFactory(privateKey); try { InputStream decrypted = pkesk.getDataStream(decryptorFactory); - in = new OpenPgpMessageInputStream(decrypted, options); + MessageMetadata.EncryptedData encryptedData = new MessageMetadata.EncryptedData( + SymmetricKeyAlgorithm.requireFromId(pkesk.getSymmetricAlgorithm(decryptorFactory))); + in = new OpenPgpMessageInputStream(decrypted, options, encryptedData); return true; } catch (PGPException e) { // hm :/ @@ -291,7 +287,9 @@ public class OpenPgpMessageInputStream extends InputStream { try { InputStream decrypted = pkesk.getDataStream(decryptorFactory); - in = new OpenPgpMessageInputStream(decrypted, options); + MessageMetadata.EncryptedData encryptedData = new MessageMetadata.EncryptedData( + SymmetricKeyAlgorithm.requireFromId(pkesk.getSymmetricAlgorithm(decryptorFactory))); + in = new OpenPgpMessageInputStream(decrypted, options, encryptedData); return true; } catch (PGPException e) { // hm :/ @@ -402,6 +400,7 @@ public class OpenPgpMessageInputStream extends InputStream { signatures.update(b); } else { in.close(); + collectMetadata(); in = null; try { @@ -426,6 +425,7 @@ public class OpenPgpMessageInputStream extends InputStream { int r = in.read(b, off, len); if (r == -1) { in.close(); + collectMetadata(); in = null; try { @@ -447,6 +447,7 @@ public class OpenPgpMessageInputStream extends InputStream { if (in != null) { in.close(); + collectMetadata(); in = null; } @@ -461,6 +462,21 @@ public class OpenPgpMessageInputStream extends InputStream { closed = true; } + private void collectMetadata() { + if (in instanceof OpenPgpMessageInputStream) { + OpenPgpMessageInputStream child = (OpenPgpMessageInputStream) in; + MessageMetadata.Layer childLayer = child.metadata; + this.metadata.setChild((MessageMetadata.Nested) childLayer); + } + } + + public MessageMetadata getMetadata() { + if (!closed) { + throw new IllegalStateException("Stream must be closed before access to metadata can be granted."); + } + return new MessageMetadata((MessageMetadata.Message) metadata); + } + private static class SortedESKs { private List skesks = new ArrayList<>(); diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/automaton/PDA.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/automaton/PDA.java index 793a3451..feb759ea 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/automaton/PDA.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/automaton/PDA.java @@ -187,10 +187,7 @@ public class PDA { } public void next(InputAlphabet input) throws MalformedOpenPgpMessageException { - State old = state; - StackAlphabet stackItem = stack.isEmpty() ? null : stack.peek(); state = state.transition(input, this); - System.out.println(id + ": Transition from " + old + " to " + state + " via " + input + " with stack " + stackItem); } /** diff --git a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/MessageMetadataTest.java b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/MessageMetadataTest.java new file mode 100644 index 00000000..9f887eb9 --- /dev/null +++ b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/MessageMetadataTest.java @@ -0,0 +1,84 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.decryption_verification; + +import org.junit.JUtils; +import org.junit.jupiter.api.Test; +import org.pgpainless.algorithm.CompressionAlgorithm; +import org.pgpainless.algorithm.StreamEncoding; +import org.pgpainless.algorithm.SymmetricKeyAlgorithm; +import org.pgpainless.util.DateUtil; + +import java.util.Date; +import java.util.Iterator; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class MessageMetadataTest { + + @Test + public void processTestMessage_COMP_ENC_ENC_LIT() { + // Note: COMP of ENC does not make sense, since ENC is indistinguishable from randomness + // and randomness cannot be encrypted. + // For the sake of testing though, this is okay. + MessageMetadata.Message message = new MessageMetadata.Message(); + + MessageMetadata.CompressedData compressedData = new MessageMetadata.CompressedData(CompressionAlgorithm.ZIP); + MessageMetadata.EncryptedData encryptedData = new MessageMetadata.EncryptedData(SymmetricKeyAlgorithm.AES_128); + MessageMetadata.EncryptedData encryptedData1 = new MessageMetadata.EncryptedData(SymmetricKeyAlgorithm.AES_256); + MessageMetadata.LiteralData literalData = new MessageMetadata.LiteralData(); + + message.setChild(compressedData); + compressedData.setChild(encryptedData); + encryptedData.setChild(encryptedData1); + encryptedData1.setChild(literalData); + + MessageMetadata metadata = new MessageMetadata(message); + + // Check encryption algs + assertEquals(SymmetricKeyAlgorithm.AES_128, metadata.getEncryptionAlgorithm(), "getEncryptionAlgorithm() returns alg of outermost EncryptedData"); + Iterator encryptionAlgs = metadata.getEncryptionAlgorithms(); + assertTrue(encryptionAlgs.hasNext(), "There is at least one EncryptedData child"); + assertTrue(encryptionAlgs.hasNext(), "The child is still there"); + assertEquals(SymmetricKeyAlgorithm.AES_128, encryptionAlgs.next(), "The first algo is AES128"); + assertTrue(encryptionAlgs.hasNext(), "There is another EncryptedData"); + assertTrue(encryptionAlgs.hasNext(), "There is *still* another EncryptedData"); + assertEquals(SymmetricKeyAlgorithm.AES_256, encryptionAlgs.next(), "The second algo is AES256"); + assertFalse(encryptionAlgs.hasNext(), "There is no more EncryptedData"); + assertFalse(encryptionAlgs.hasNext(), "There *still* is no more EncryptedData"); + + assertEquals(CompressionAlgorithm.ZIP, metadata.getCompressionAlgorithm(), "getCompressionAlgorithm() returns alg of outermost CompressedData"); + Iterator compAlgs = metadata.getCompressionAlgorithms(); + assertTrue(compAlgs.hasNext()); + assertTrue(compAlgs.hasNext()); + assertEquals(CompressionAlgorithm.ZIP, compAlgs.next()); + assertFalse(compAlgs.hasNext()); + assertFalse(compAlgs.hasNext()); + + assertEquals("", metadata.getFilename()); + JUtils.assertDateEquals(new Date(0L), metadata.getModificationDate()); + assertEquals(StreamEncoding.BINARY, metadata.getFormat()); + } + + @Test + public void testProcessLiteralDataMessage() { + MessageMetadata.LiteralData literalData = new MessageMetadata.LiteralData( + "collateral_murder.zip", + DateUtil.parseUTCDate("2010-04-05 10:12:03 UTC"), + StreamEncoding.BINARY); + MessageMetadata.Message message = new MessageMetadata.Message(); + message.setChild(literalData); + + MessageMetadata metadata = new MessageMetadata(message); + assertNull(metadata.getCompressionAlgorithm()); + assertNull(metadata.getEncryptionAlgorithm()); + assertEquals("collateral_murder.zip", metadata.getFilename()); + assertEquals(DateUtil.parseUTCDate("2010-04-05 10:12:03 UTC"), metadata.getModificationDate()); + assertEquals(StreamEncoding.BINARY, metadata.getFormat()); + } +} diff --git a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStreamTest.java b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStreamTest.java index af1fac44..8219ec68 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStreamTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStreamTest.java @@ -1,5 +1,21 @@ package org.pgpainless.decryption_verification; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.nio.charset.StandardCharsets; +import java.security.InvalidAlgorithmParameterException; +import java.security.NoSuchAlgorithmException; +import java.util.Date; +import java.util.Iterator; +import java.util.stream.Stream; + import org.bouncycastle.bcpg.ArmoredInputStream; import org.bouncycastle.bcpg.ArmoredOutputStream; import org.bouncycastle.bcpg.CompressionAlgorithmTags; @@ -11,9 +27,14 @@ import org.bouncycastle.openpgp.PGPPublicKeyRing; import org.bouncycastle.openpgp.PGPSecretKeyRing; import org.bouncycastle.openpgp.PGPSignature; import org.bouncycastle.util.io.Streams; -import org.junit.jupiter.api.Test; +import org.junit.JUtils; +import org.junit.jupiter.api.Named; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; import org.pgpainless.PGPainless; import org.pgpainless.algorithm.CompressionAlgorithm; +import org.pgpainless.algorithm.StreamEncoding; import org.pgpainless.encryption_signing.EncryptionOptions; import org.pgpainless.encryption_signing.EncryptionResult; import org.pgpainless.encryption_signing.EncryptionStream; @@ -23,17 +44,7 @@ import org.pgpainless.exception.MalformedOpenPgpMessageException; import org.pgpainless.key.protection.SecretKeyRingProtector; import org.pgpainless.util.ArmoredInputStreamFactory; import org.pgpainless.util.Passphrase; - -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.OutputStream; -import java.nio.charset.StandardCharsets; -import java.security.InvalidAlgorithmParameterException; -import java.security.NoSuchAlgorithmException; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertThrows; +import org.pgpainless.util.Tuple; public class OpenPgpMessageInputStreamTest { @@ -304,107 +315,119 @@ public class OpenPgpMessageInputStreamTest { System.out.println(out); } - @Test - public void testProcessLIT() throws IOException, PGPException { - String plain = processReadBuffered(LIT, ConsumerOptions.get()); - assertEquals(PLAINTEXT, plain); - - plain = processReadSequential(LIT, ConsumerOptions.get()); - assertEquals(PLAINTEXT, plain); + interface Processor { + Tuple process(String armoredMessage, ConsumerOptions options) throws PGPException, IOException; } - @Test - public void testProcessLIT_LIT_fails() { + private static Stream provideMessageProcessors() { + return Stream.of( + Arguments.of(Named.of("read(buf,off,len)", (Processor) OpenPgpMessageInputStreamTest::processReadBuffered)), + Arguments.of(Named.of("read()", (Processor) OpenPgpMessageInputStreamTest::processReadSequential))); + } + + @ParameterizedTest(name = "Process LIT using {0}") + @MethodSource("provideMessageProcessors") + public void testProcessLIT(Processor processor) throws IOException, PGPException { + Tuple result = processor.process(LIT, ConsumerOptions.get()); + String plain = result.getA(); + assertEquals(PLAINTEXT, plain); + + MessageMetadata metadata = result.getB(); + assertNull(metadata.getCompressionAlgorithm()); + assertNull(metadata.getEncryptionAlgorithm()); + assertEquals("", metadata.getFilename()); + JUtils.assertDateEquals(new Date(0L), metadata.getModificationDate()); + assertEquals(StreamEncoding.BINARY, metadata.getFormat()); + } + + @ParameterizedTest(name = "Process LIT LIT using {0}") + @MethodSource("provideMessageProcessors") + public void testProcessLIT_LIT_fails(Processor processor) { assertThrows(MalformedOpenPgpMessageException.class, - () -> processReadBuffered(LIT_LIT, ConsumerOptions.get())); + () -> processor.process(LIT_LIT, ConsumerOptions.get())); + } + @ParameterizedTest(name = "Process COMP(LIT) using {0}") + @MethodSource("provideMessageProcessors") + public void testProcessCOMP_LIT(Processor processor) throws PGPException, IOException { + Tuple result = processor.process(COMP_LIT, ConsumerOptions.get()); + String plain = result.getA(); + assertEquals(PLAINTEXT, plain); + MessageMetadata metadata = result.getB(); + assertEquals(CompressionAlgorithm.ZIP, metadata.getCompressionAlgorithm()); + } + + @ParameterizedTest(name = "Process COMP using {0}") + @MethodSource("provideMessageProcessors") + public void testProcessCOMP_fails(Processor processor) { assertThrows(MalformedOpenPgpMessageException.class, - () -> processReadSequential(LIT_LIT, ConsumerOptions.get())); + () -> processor.process(COMP, ConsumerOptions.get())); } - @Test - public void testProcessCOMP_LIT() throws PGPException, IOException { - String plain = processReadBuffered(COMP_LIT, ConsumerOptions.get()); - assertEquals(PLAINTEXT, plain); - - plain = processReadSequential(COMP_LIT, ConsumerOptions.get()); + @ParameterizedTest(name = "Process COMP(COMP(LIT)) using {0}") + @MethodSource("provideMessageProcessors") + public void testProcessCOMP_COMP_LIT(Processor processor) throws PGPException, IOException { + Tuple result = processor.process(COMP_COMP_LIT, ConsumerOptions.get()); + String plain = result.getA(); assertEquals(PLAINTEXT, plain); + MessageMetadata metadata = result.getB(); + assertEquals(CompressionAlgorithm.ZIP, metadata.getCompressionAlgorithm()); + Iterator compressionAlgorithms = metadata.getCompressionAlgorithms(); + assertEquals(CompressionAlgorithm.ZIP, compressionAlgorithms.next()); + assertEquals(CompressionAlgorithm.BZIP2, compressionAlgorithms.next()); + assertFalse(compressionAlgorithms.hasNext()); } - @Test - public void testProcessCOMP_fails() { - assertThrows(MalformedOpenPgpMessageException.class, - () -> processReadBuffered(COMP, ConsumerOptions.get())); - - assertThrows(MalformedOpenPgpMessageException.class, - () -> processReadSequential(COMP, ConsumerOptions.get())); - } - - @Test - public void testProcessCOMP_COMP_LIT() throws PGPException, IOException { - String plain = processReadBuffered(COMP_COMP_LIT, ConsumerOptions.get()); - assertEquals(PLAINTEXT, plain); - - plain = processReadSequential(COMP_COMP_LIT, ConsumerOptions.get()); - assertEquals(PLAINTEXT, plain); - } - - @Test - public void testProcessSIG_LIT() throws PGPException, IOException { + @ParameterizedTest(name = "Process SIG LIT using {0}") + @MethodSource("provideMessageProcessors") + public void testProcessSIG_LIT(Processor processor) throws PGPException, IOException { PGPPublicKeyRing cert = PGPainless.extractCertificate( PGPainless.readKeyRing().secretKeyRing(KEY)); - String plain = processReadBuffered(SIG_LIT, ConsumerOptions.get() - .addVerificationCert(cert)); - assertEquals(PLAINTEXT, plain); - - plain = processReadSequential(SIG_LIT, ConsumerOptions.get() + Tuple result = processor.process(SIG_LIT, ConsumerOptions.get() .addVerificationCert(cert)); + String plain = result.getA(); assertEquals(PLAINTEXT, plain); } - @Test - public void testProcessSENC_LIT() throws PGPException, IOException { - String plain = processReadBuffered(SENC_LIT, ConsumerOptions.get().addDecryptionPassphrase(Passphrase.fromPassword(PASSPHRASE))); - assertEquals(PLAINTEXT, plain); - - plain = processReadSequential(SENC_LIT, ConsumerOptions.get().addDecryptionPassphrase(Passphrase.fromPassword(PASSPHRASE))); + @ParameterizedTest(name = "Process SENC(LIT) using {0}") + @MethodSource("provideMessageProcessors") + public void testProcessSENC_LIT(Processor processor) throws PGPException, IOException { + Tuple result = processor.process(SENC_LIT, ConsumerOptions.get().addDecryptionPassphrase(Passphrase.fromPassword(PASSPHRASE))); + String plain = result.getA(); assertEquals(PLAINTEXT, plain); } - @Test - public void testProcessPENC_COMP_LIT() throws IOException, PGPException { + @ParameterizedTest(name = "Process PENC(LIT) using {0}") + @MethodSource("provideMessageProcessors") + public void testProcessPENC_COMP_LIT(Processor processor) throws IOException, PGPException { PGPSecretKeyRing secretKeys = PGPainless.readKeyRing().secretKeyRing(KEY); - String plain = processReadBuffered(PENC_COMP_LIT, ConsumerOptions.get() - .addDecryptionKey(secretKeys)); - assertEquals(PLAINTEXT, plain); - - plain = processReadSequential(PENC_COMP_LIT, ConsumerOptions.get() + Tuple result = processor.process(PENC_COMP_LIT, ConsumerOptions.get() .addDecryptionKey(secretKeys)); + String plain = result.getA(); assertEquals(PLAINTEXT, plain); } - @Test - public void testProcessOPS_LIT_SIG() throws IOException, PGPException { + @ParameterizedTest(name = "Process OPS LIT SIG using {0}") + @MethodSource("provideMessageProcessors") + public void testProcessOPS_LIT_SIG(Processor processor) throws IOException, PGPException { PGPPublicKeyRing cert = PGPainless.extractCertificate(PGPainless.readKeyRing().secretKeyRing(KEY)); - String plain = processReadBuffered(OPS_LIT_SIG, ConsumerOptions.get() - .addVerificationCert(cert)); - assertEquals(PLAINTEXT, plain); - - plain = processReadSequential(OPS_LIT_SIG, ConsumerOptions.get() + Tuple result = processor.process(OPS_LIT_SIG, ConsumerOptions.get() .addVerificationCert(cert)); + String plain = result.getA(); assertEquals(PLAINTEXT, plain); } - private String processReadBuffered(String armoredMessage, ConsumerOptions options) throws PGPException, IOException { + private static Tuple processReadBuffered(String armoredMessage, ConsumerOptions options) throws PGPException, IOException { OpenPgpMessageInputStream in = get(armoredMessage, options); ByteArrayOutputStream out = new ByteArrayOutputStream(); Streams.pipeAll(in, out); in.close(); - return out.toString(); + MessageMetadata metadata = in.getMetadata(); + return new Tuple<>(out.toString(), metadata); } - private String processReadSequential(String armoredMessage, ConsumerOptions options) throws PGPException, IOException { + private static Tuple processReadSequential(String armoredMessage, ConsumerOptions options) throws PGPException, IOException { OpenPgpMessageInputStream in = get(armoredMessage, options); ByteArrayOutputStream out = new ByteArrayOutputStream(); @@ -414,10 +437,11 @@ public class OpenPgpMessageInputStreamTest { } in.close(); - return out.toString(); + MessageMetadata metadata = in.getMetadata(); + return new Tuple<>(out.toString(), metadata); } - private OpenPgpMessageInputStream get(String armored, ConsumerOptions options) throws IOException, PGPException { + private static OpenPgpMessageInputStream get(String armored, ConsumerOptions options) throws IOException, PGPException { ByteArrayInputStream bytesIn = new ByteArrayInputStream(armored.getBytes(StandardCharsets.UTF_8)); ArmoredInputStream armorIn = ArmoredInputStreamFactory.get(bytesIn); OpenPgpMessageInputStream pgpIn = new OpenPgpMessageInputStream(armorIn, options);