From 7b76a9162d407d0844ab2de2cecc6cdc27a8abe6 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 8 Sep 2022 18:23:20 +0200 Subject: [PATCH 01/80] Add Pushdown Automaton for checking OpenPGP message syntax The automaton implements what is described in https://github.com/pgpainless/pgpainless/blob/main/misc/OpenPGPMessageFormat.md However, some differences exist to adopt it to BouncyCastle Part of #237 --- .../PushdownAutomaton.java | 391 ++++++++++++++++++ .../MalformedOpenPgpMessageException.java | 31 ++ .../PushDownAutomatonTest.java | 205 +++++++++ 3 files changed, 627 insertions(+) create mode 100644 pgpainless-core/src/main/java/org/pgpainless/decryption_verification/PushdownAutomaton.java create mode 100644 pgpainless-core/src/main/java/org/pgpainless/exception/MalformedOpenPgpMessageException.java create mode 100644 pgpainless-core/src/test/java/org/pgpainless/decryption_verification/PushDownAutomatonTest.java diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/PushdownAutomaton.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/PushdownAutomaton.java new file mode 100644 index 00000000..6930de4b --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/PushdownAutomaton.java @@ -0,0 +1,391 @@ +package org.pgpainless.decryption_verification; + +import org.bouncycastle.openpgp.PGPCompressedData; +import org.bouncycastle.openpgp.PGPEncryptedDataList; +import org.bouncycastle.openpgp.PGPLiteralData; +import org.bouncycastle.openpgp.PGPOnePassSignatureList; +import org.bouncycastle.openpgp.PGPSignatureList; +import org.pgpainless.exception.MalformedOpenPgpMessageException; + +import java.util.Stack; + +import static org.pgpainless.decryption_verification.PushdownAutomaton.StackAlphabet.msg; +import static org.pgpainless.decryption_verification.PushdownAutomaton.StackAlphabet.ops; +import static org.pgpainless.decryption_verification.PushdownAutomaton.StackAlphabet.terminus; + +/** + * Pushdown Automaton to verify the correct syntax of OpenPGP messages during decryption. + *

+ * OpenPGP messages MUST follow certain rules in order to be well-formed. + * Section §11.3. of RFC4880 specifies a formal grammar for OpenPGP messages. + *

+ * This grammar was transformed into a pushdown automaton, which is implemented below. + * The automaton only ends up in a valid state ({@link #isValid()} iff the OpenPGP message conformed to the + * grammar. + *

+ * There are some specialties with this implementation though: + * Bouncy Castle combines ESKs and Encrypted Data Packets into a single object, so we do not have to + * handle those manually. + *

+ * Bouncy Castle further combines OnePassSignatures and Signatures into lists, so instead of pushing multiple + * 'o's onto the stack repeatedly, a sequence of OnePassSignatures causes a single 'o' to be pushed to the stack. + * The same is true for Signatures. + *

+ * Therefore, a message is valid, even if the number of OnePassSignatures and Signatures does not match. + * If a message contains at least one OnePassSignature, it is sufficient if there is at least one Signature to + * not cause a {@link MalformedOpenPgpMessageException}. + * + * @see RFC4880 §11.3. OpenPGP Messages + */ +public class PushdownAutomaton { + + public enum InputAlphabet { + /** + * A {@link PGPLiteralData} packet. + */ + LiteralData, + /** + * A {@link PGPSignatureList} object. + */ + Signatures, + /** + * A {@link PGPOnePassSignatureList} object. + */ + OnePassSignatures, + /** + * A {@link PGPCompressedData} packet. + * The contents of this packet MUST form a valid OpenPGP message, so a nested PDA is opened to verify + * its nested packet sequence. + */ + CompressedData, + /** + * A {@link PGPEncryptedDataList} object. + * This object combines multiple ESKs and the corresponding Symmetrically Encrypted + * (possibly Integrity Protected) Data packet. + */ + EncryptedData, + /** + * Marks the end of a (sub-) sequence. + * This input is given if the end of an OpenPGP message is reached. + * This might be the case for the end of the whole ciphertext, or the end of a packet with nested contents + * (e.g. the end of a Compressed Data packet). + */ + EndOfSequence + } + + public enum StackAlphabet { + /** + * OpenPGP Message. + */ + msg, + /** + * OnePassSignature (in case of BC this represents a OnePassSignatureList). + */ + ops, + /** + * ESK. Not used, as BC combines encrypted data with their encrypted session keys. + */ + esk, + /** + * Special symbol representing the end of the message. + */ + terminus + } + + /** + * Set of states of the automaton. + * Each state defines its valid transitions in their {@link State#transition(InputAlphabet, PushdownAutomaton)} + * method. + */ + public enum State { + + OpenPgpMessage { + @Override + State transition(InputAlphabet input, PushdownAutomaton automaton) throws MalformedOpenPgpMessageException { + StackAlphabet stackItem = automaton.popStack(); + if (stackItem != msg) { + throw new MalformedOpenPgpMessageException(this, input, stackItem); + } + switch (input) { + + case LiteralData: + return LiteralMessage; + + case Signatures: + automaton.pushStack(msg); + return OpenPgpMessage; + + case OnePassSignatures: + automaton.pushStack(ops); + automaton.pushStack(msg); + return OpenPgpMessage; + + case CompressedData: + return CompressedMessage; + + case EncryptedData: + return EncryptedMessage; + + case EndOfSequence: + default: + throw new MalformedOpenPgpMessageException(this, input, stackItem); + } + } + }, + + LiteralMessage { + @Override + State transition(InputAlphabet input, PushdownAutomaton automaton) throws MalformedOpenPgpMessageException { + StackAlphabet stackItem = automaton.popStack(); + switch (input) { + + case Signatures: + if (stackItem == ops) { + return CorrespondingSignature; + } else { + throw new MalformedOpenPgpMessageException(this, input, stackItem); + } + + case EndOfSequence: + if (stackItem == terminus && automaton.stack.isEmpty()) { + return Valid; + } else { + throw new MalformedOpenPgpMessageException(this, input, stackItem); + } + + case LiteralData: + case OnePassSignatures: + case CompressedData: + case EncryptedData: + default: + throw new MalformedOpenPgpMessageException(this, input, stackItem); + } + } + }, + + CompressedMessage { + @Override + State transition(InputAlphabet input, PushdownAutomaton automaton) throws MalformedOpenPgpMessageException { + StackAlphabet stackItem = automaton.popStack(); + switch (input) { + case Signatures: + if (stackItem == ops) { + return CorrespondingSignature; + } else { + throw new MalformedOpenPgpMessageException(this, input, stackItem); + } + + case EndOfSequence: + if (stackItem == terminus && automaton.stack.isEmpty()) { + return Valid; + } else { + throw new MalformedOpenPgpMessageException(this, input, stackItem); + } + + case LiteralData: + case OnePassSignatures: + case CompressedData: + case EncryptedData: + default: + throw new MalformedOpenPgpMessageException(this, input, stackItem); + } + } + }, + + EncryptedMessage { + @Override + State transition(InputAlphabet input, PushdownAutomaton automaton) throws MalformedOpenPgpMessageException { + StackAlphabet stackItem = automaton.popStack(); + switch (input) { + case Signatures: + if (stackItem == ops) { + return CorrespondingSignature; + } else { + throw new MalformedOpenPgpMessageException(this, input, stackItem); + } + + case EndOfSequence: + if (stackItem == terminus && automaton.stack.isEmpty()) { + return Valid; + } else { + throw new MalformedOpenPgpMessageException(this, input, stackItem); + } + + case LiteralData: + case OnePassSignatures: + case CompressedData: + case EncryptedData: + default: + throw new MalformedOpenPgpMessageException(this, input, stackItem); + } + } + }, + + CorrespondingSignature { + @Override + State transition(InputAlphabet input, PushdownAutomaton automaton) throws MalformedOpenPgpMessageException { + StackAlphabet stackItem = automaton.popStack(); + if (stackItem == terminus && input == InputAlphabet.EndOfSequence && automaton.stack.isEmpty()) { + return Valid; + } else { + throw new MalformedOpenPgpMessageException(this, input, stackItem); + } + } + }, + + Valid { + @Override + State transition(InputAlphabet input, PushdownAutomaton automaton) throws MalformedOpenPgpMessageException { + throw new MalformedOpenPgpMessageException(this, input, null); + } + }, + ; + + /** + * Pop the automatons stack and transition to another state. + * If no valid transition from the current state is available given the popped stack item and input symbol, + * a {@link MalformedOpenPgpMessageException} is thrown. + * Otherwise, the stack is manipulated according to the valid transition and the new state is returned. + * + * @param input input symbol + * @param automaton automaton + * @return new state of the automaton + * @throws MalformedOpenPgpMessageException in case of an illegal input symbol + */ + abstract State transition(InputAlphabet input, PushdownAutomaton automaton) throws MalformedOpenPgpMessageException; + } + + private final Stack stack = new Stack<>(); + private State state; + // Some OpenPGP packets have nested contents (e.g. compressed / encrypted data). + PushdownAutomaton nestedSequence = null; + + public PushdownAutomaton() { + state = State.OpenPgpMessage; + stack.push(terminus); + stack.push(msg); + } + + /** + * Process the next input packet. + * + * @param input input + * @throws MalformedOpenPgpMessageException in case the input packet is illegal here + */ + public void next(InputAlphabet input) throws MalformedOpenPgpMessageException { + _next(input); + } + + /** + * Process the next input packet. + * This method returns true, iff the given input triggered a successful closing of this PDAs nested PDA. + *

+ * This is for example the case, if the current packet is a Compressed Data packet which contains a + * valid nested OpenPGP message and the last input was {@link InputAlphabet#EndOfSequence} indicating the + * end of the Compressed Data packet. + *

+ * If the input triggered this PDAs nested PDA to close its nested PDA, this method returns false + * in order to prevent this PDA from closing its nested PDA prematurely. + * + * @param input input + * @return true if this just closed its nested sequence, false otherwise + * @throws MalformedOpenPgpMessageException if the input is illegal + */ + private boolean _next(InputAlphabet input) throws MalformedOpenPgpMessageException { + if (nestedSequence != null) { + boolean sequenceInNestedSequenceWasClosed = nestedSequence._next(input); + if (sequenceInNestedSequenceWasClosed) return false; // No need to close out nested sequence too. + } else { + // make a state transition in this automaton + state = state.transition(input, this); + + // If the processed packet contains nested sequence, open nested automaton for it + if (input == InputAlphabet.CompressedData || input == InputAlphabet.EncryptedData) { + nestedSequence = new PushdownAutomaton(); + } + } + + if (input != InputAlphabet.EndOfSequence) { + return false; + } + + // Close nested sequence if needed + boolean nestedIsInnerMost = nestedSequence != null && nestedSequence.isInnerMost(); + if (nestedIsInnerMost) { + if (nestedSequence.isValid()) { + // Close nested sequence + nestedSequence = null; + return true; + } else { + throw new MalformedOpenPgpMessageException("Climbing up nested message validation failed." + + " Automaton for current nesting level is not in valid state: " + nestedSequence.getState() + " " + nestedSequence.stack.peek() + " (Input was " + input + ")"); + } + } + return false; + } + + /** + * Return the current state of the PDA. + * + * @return state + */ + private State getState() { + return state; + } + + /** + * Return true, if the PDA is in a valid state (the OpenPGP message is valid). + * + * @return true if valid, false otherwise + */ + public boolean isValid() { + return getState() == State.Valid && stack.isEmpty(); + } + + /** + * Pop an item from the stack. + * + * @return stack item + */ + private StackAlphabet popStack() { + return stack.pop(); + } + + /** + * Push an item onto the stack. + * + * @param item item + */ + private void pushStack(StackAlphabet item) { + stack.push(item); + } + + /** + * Return true, if this packet sequence has no nested sequence. + * A nested sequence is for example the content of a Compressed Data packet. + * + * @return true if PDA is innermost, false if it has a nested sequence + */ + private boolean isInnerMost() { + return nestedSequence == null; + } + + @Override + public String toString() { + StringBuilder out = new StringBuilder("State: ").append(state) + .append(", Stack (asc.): ").append(stack) + .append('\n'); + if (nestedSequence != null) { + // recursively call toString() on nested PDAs and indent their representation + String nestedToString = nestedSequence.toString(); + String[] lines = nestedToString.split("\n"); + for (int i = 0; i < lines.length; i++) { + String nestedLine = lines[i]; + out.append(i == 0 ? "⤷ " : " ") // indent nested PDA + .append(nestedLine) + .append('\n'); + } + } + return out.toString(); + } +} diff --git a/pgpainless-core/src/main/java/org/pgpainless/exception/MalformedOpenPgpMessageException.java b/pgpainless-core/src/main/java/org/pgpainless/exception/MalformedOpenPgpMessageException.java new file mode 100644 index 00000000..07fed365 --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/exception/MalformedOpenPgpMessageException.java @@ -0,0 +1,31 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.exception; + +import org.bouncycastle.openpgp.PGPException; +import org.pgpainless.decryption_verification.PushdownAutomaton; + +/** + * Exception that gets thrown if the OpenPGP message is malformed. + * Malformed messages are messages which do not follow the grammar specified in the RFC. + * + * @see RFC4880 §11.3. OpenPGP Messages + */ +public class MalformedOpenPgpMessageException extends PGPException { + + public MalformedOpenPgpMessageException(String message) { + super(message); + } + + public MalformedOpenPgpMessageException(String message, MalformedOpenPgpMessageException cause) { + super(message, cause); + } + + public MalformedOpenPgpMessageException(PushdownAutomaton.State state, + PushdownAutomaton.InputAlphabet input, + PushdownAutomaton.StackAlphabet stackItem) { + this("Invalid input: There is no legal transition from state '" + state + "' for input '" + input + "' when '" + stackItem + "' is on top of the stack."); + } +} diff --git a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/PushDownAutomatonTest.java b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/PushDownAutomatonTest.java new file mode 100644 index 00000000..1bd07308 --- /dev/null +++ b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/PushDownAutomatonTest.java @@ -0,0 +1,205 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.decryption_verification; + +import org.junit.jupiter.api.Test; +import org.pgpainless.exception.MalformedOpenPgpMessageException; + +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class PushDownAutomatonTest { + + /** + * MSG is valid. + * + * @throws MalformedOpenPgpMessageException fail + */ + @Test + public void testSimpleLiteralMessageIsValid() throws MalformedOpenPgpMessageException { + PushdownAutomaton automaton = new PushdownAutomaton(); + automaton.next(PushdownAutomaton.InputAlphabet.LiteralData); + automaton.next(PushdownAutomaton.InputAlphabet.EndOfSequence); + + assertTrue(automaton.isValid()); + } + + /** + * OPS MSG SIG is valid. + * + * @throws MalformedOpenPgpMessageException fail + */ + @Test + public void testSimpleOpsSignedMesssageIsValid() throws MalformedOpenPgpMessageException { + PushdownAutomaton automaton = new PushdownAutomaton(); + automaton.next(PushdownAutomaton.InputAlphabet.OnePassSignatures); + automaton.next(PushdownAutomaton.InputAlphabet.LiteralData); + automaton.next(PushdownAutomaton.InputAlphabet.Signatures); + automaton.next(PushdownAutomaton.InputAlphabet.EndOfSequence); + + assertTrue(automaton.isValid()); + } + + /** + * SIG MSG is valid. + * + * @throws MalformedOpenPgpMessageException fail + */ + @Test + public void testSimplePrependSignedMessageIsValid() throws MalformedOpenPgpMessageException { + PushdownAutomaton automaton = new PushdownAutomaton(); + automaton.next(PushdownAutomaton.InputAlphabet.Signatures); + automaton.next(PushdownAutomaton.InputAlphabet.LiteralData); + automaton.next(PushdownAutomaton.InputAlphabet.EndOfSequence); + + assertTrue(automaton.isValid()); + } + + /** + * OPS COMP(MSG) SIG is valid. + * + * @throws MalformedOpenPgpMessageException fail + */ + @Test + public void testOPSSignedCompressedMessageIsValid() throws MalformedOpenPgpMessageException { + PushdownAutomaton automaton = new PushdownAutomaton(); + automaton.next(PushdownAutomaton.InputAlphabet.OnePassSignatures); + automaton.next(PushdownAutomaton.InputAlphabet.CompressedData); + automaton.next(PushdownAutomaton.InputAlphabet.LiteralData); + automaton.next(PushdownAutomaton.InputAlphabet.EndOfSequence); + automaton.next(PushdownAutomaton.InputAlphabet.Signatures); + automaton.next(PushdownAutomaton.InputAlphabet.EndOfSequence); + + assertTrue(automaton.isValid()); + } + + /** + * OPS ENC(COMP(COMP(MSG))) SIG is valid. + * + * @throws MalformedOpenPgpMessageException fail + */ + @Test + public void testOpsSignedEncryptedCompressedCompressedMessageIsValid() throws MalformedOpenPgpMessageException { + PushdownAutomaton automaton = new PushdownAutomaton(); + automaton.next(PushdownAutomaton.InputAlphabet.OnePassSignatures); + automaton.next(PushdownAutomaton.InputAlphabet.EncryptedData); + automaton.next(PushdownAutomaton.InputAlphabet.CompressedData); + automaton.next(PushdownAutomaton.InputAlphabet.CompressedData); + + automaton.next(PushdownAutomaton.InputAlphabet.LiteralData); + + automaton.next(PushdownAutomaton.InputAlphabet.EndOfSequence); + automaton.next(PushdownAutomaton.InputAlphabet.EndOfSequence); + automaton.next(PushdownAutomaton.InputAlphabet.EndOfSequence); + automaton.next(PushdownAutomaton.InputAlphabet.Signatures); + automaton.next(PushdownAutomaton.InputAlphabet.EndOfSequence); + + assertTrue(automaton.isValid()); + } + + /** + * MSG SIG is invalid. + * + * @throws MalformedOpenPgpMessageException fail + */ + @Test + public void testLiteralPlusSigsFails() throws MalformedOpenPgpMessageException { + PushdownAutomaton automaton = new PushdownAutomaton(); + automaton.next(PushdownAutomaton.InputAlphabet.LiteralData); + assertThrows(MalformedOpenPgpMessageException.class, + () -> automaton.next(PushdownAutomaton.InputAlphabet.Signatures)); + } + + /** + * MSG MSG is invalid. + * + * @throws MalformedOpenPgpMessageException fail + */ + @Test + public void testTwoLiteralDataPacketsFails() throws MalformedOpenPgpMessageException { + PushdownAutomaton automaton = new PushdownAutomaton(); + automaton.next(PushdownAutomaton.InputAlphabet.LiteralData); + assertThrows(MalformedOpenPgpMessageException.class, + () -> automaton.next(PushdownAutomaton.InputAlphabet.LiteralData)); + } + + /** + * OPS COMP(MSG MSG) SIG is invalid (two literal packets are illegal). + * + * @throws MalformedOpenPgpMessageException fail + */ + @Test + public void testOPSSignedMessageWithTwoLiteralDataPacketsFails() throws MalformedOpenPgpMessageException { + PushdownAutomaton automaton = new PushdownAutomaton(); + automaton.next(PushdownAutomaton.InputAlphabet.OnePassSignatures); + automaton.next(PushdownAutomaton.InputAlphabet.CompressedData); + automaton.next(PushdownAutomaton.InputAlphabet.LiteralData); + assertThrows(MalformedOpenPgpMessageException.class, + () -> automaton.next(PushdownAutomaton.InputAlphabet.LiteralData)); + } + + /** + * OPS COMP(MSG) MSG SIG is invalid. + * + * @throws MalformedOpenPgpMessageException fail + */ + @Test + public void testOPSSignedMessageWithTwoLiteralDataPacketsFails2() throws MalformedOpenPgpMessageException { + PushdownAutomaton automaton = new PushdownAutomaton(); + automaton.next(PushdownAutomaton.InputAlphabet.OnePassSignatures); + automaton.next(PushdownAutomaton.InputAlphabet.CompressedData); + automaton.next(PushdownAutomaton.InputAlphabet.LiteralData); + automaton.next(PushdownAutomaton.InputAlphabet.EndOfSequence); + assertThrows(MalformedOpenPgpMessageException.class, + () -> automaton.next(PushdownAutomaton.InputAlphabet.LiteralData)); + } + + /** + * OPS COMP(MSG SIG) is invalid (MSG SIG does not form valid nested message). + * + * @throws MalformedOpenPgpMessageException fail + */ + @Test + public void testCorrespondingSignaturesOfOpsSignedMessageAreLayerFurtherDownFails() throws MalformedOpenPgpMessageException { + PushdownAutomaton automaton = new PushdownAutomaton(); + automaton.next(PushdownAutomaton.InputAlphabet.OnePassSignatures); + automaton.next(PushdownAutomaton.InputAlphabet.CompressedData); + automaton.next(PushdownAutomaton.InputAlphabet.LiteralData); + assertThrows(MalformedOpenPgpMessageException.class, + () -> automaton.next(PushdownAutomaton.InputAlphabet.Signatures)); + } + + /** + * Empty COMP is invalid. + */ + @Test + public void testEmptyCompressedDataIsInvalid() throws MalformedOpenPgpMessageException { + PushdownAutomaton automaton = new PushdownAutomaton(); + automaton.next(PushdownAutomaton.InputAlphabet.CompressedData); + assertThrows(MalformedOpenPgpMessageException.class, + () -> automaton.next(PushdownAutomaton.InputAlphabet.EndOfSequence)); + } + + @Test + public void testOPSSignedEncryptedCompressedOPSSignedMessageIsValid() throws MalformedOpenPgpMessageException { + PushdownAutomaton automaton = new PushdownAutomaton(); + automaton.next(PushdownAutomaton.InputAlphabet.OnePassSignatures); + + automaton.next(PushdownAutomaton.InputAlphabet.EncryptedData); + automaton.next(PushdownAutomaton.InputAlphabet.OnePassSignatures); + + automaton.next(PushdownAutomaton.InputAlphabet.CompressedData); + automaton.next(PushdownAutomaton.InputAlphabet.LiteralData); + automaton.next(PushdownAutomaton.InputAlphabet.EndOfSequence); + + automaton.next(PushdownAutomaton.InputAlphabet.Signatures); + automaton.next(PushdownAutomaton.InputAlphabet.EndOfSequence); + + automaton.next(PushdownAutomaton.InputAlphabet.Signatures); + automaton.next(PushdownAutomaton.InputAlphabet.EndOfSequence); + + assertTrue(automaton.isValid()); + } +} From 60d6289c4d4f1570d99b373de42abc83d9c5d6ad Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 12 Sep 2022 15:28:54 +0200 Subject: [PATCH 02/80] WIP: Implement custom PGPDecryptionStream --- .../pgpainless/algorithm/OpenPgpPacket.java | 71 +++++ .../PGPDecryptionStream.java | 238 ++++++++++++++ .../PushdownAutomaton.java | 6 + .../MalformedOpenPgpMessageException.java | 11 + .../algorithm/OpenPgpPacketTest.java | 38 +++ .../PGPDecryptionStreamTest.java | 290 ++++++++++++++++++ 6 files changed, 654 insertions(+) create mode 100644 pgpainless-core/src/main/java/org/pgpainless/algorithm/OpenPgpPacket.java create mode 100644 pgpainless-core/src/main/java/org/pgpainless/decryption_verification/PGPDecryptionStream.java create mode 100644 pgpainless-core/src/test/java/org/pgpainless/algorithm/OpenPgpPacketTest.java create mode 100644 pgpainless-core/src/test/java/org/pgpainless/decryption_verification/PGPDecryptionStreamTest.java diff --git a/pgpainless-core/src/main/java/org/pgpainless/algorithm/OpenPgpPacket.java b/pgpainless-core/src/main/java/org/pgpainless/algorithm/OpenPgpPacket.java new file mode 100644 index 00000000..63d14a31 --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/algorithm/OpenPgpPacket.java @@ -0,0 +1,71 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.algorithm; + +import org.bouncycastle.bcpg.PacketTags; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.util.HashMap; +import java.util.Map; +import java.util.NoSuchElementException; + +public enum OpenPgpPacket { + PKESK(PacketTags.PUBLIC_KEY_ENC_SESSION), + SIG(PacketTags.SIGNATURE), + SKESK(PacketTags.SYMMETRIC_KEY_ENC_SESSION), + OPS(PacketTags.ONE_PASS_SIGNATURE), + SK(PacketTags.SECRET_KEY), + PK(PacketTags.PUBLIC_KEY), + SSK(PacketTags.SECRET_SUBKEY), + COMP(PacketTags.COMPRESSED_DATA), + SED(PacketTags.SYMMETRIC_KEY_ENC), + MARKER(PacketTags.MARKER), + LIT(PacketTags.LITERAL_DATA), + TRUST(PacketTags.TRUST), + UID(PacketTags.USER_ID), + PSK(PacketTags.PUBLIC_SUBKEY), + UATTR(PacketTags.USER_ATTRIBUTE), + SEIPD(PacketTags.SYM_ENC_INTEGRITY_PRO), + MOD(PacketTags.MOD_DETECTION_CODE), + + EXP_1(PacketTags.EXPERIMENTAL_1), + EXP_2(PacketTags.EXPERIMENTAL_2), + EXP_3(PacketTags.EXPERIMENTAL_3), + EXP_4(PacketTags.EXPERIMENTAL_4), + ; + + static final Map MAP = new HashMap<>(); + + static { + for (OpenPgpPacket p : OpenPgpPacket.values()) { + MAP.put(p.getTag(), p); + } + } + + final int tag; + + @Nullable + public static OpenPgpPacket fromTag(int tag) { + return MAP.get(tag); + } + + @Nonnull + public static OpenPgpPacket requireFromTag(int tag) { + OpenPgpPacket p = fromTag(tag); + if (p == null) { + throw new NoSuchElementException("No OpenPGP packet known for tag " + tag); + } + return p; + } + + OpenPgpPacket(int tag) { + this.tag = tag; + } + + int getTag() { + return tag; + } +} diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/PGPDecryptionStream.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/PGPDecryptionStream.java new file mode 100644 index 00000000..c3053555 --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/PGPDecryptionStream.java @@ -0,0 +1,238 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.decryption_verification; + +import org.bouncycastle.bcpg.BCPGInputStream; +import org.bouncycastle.bcpg.BCPGOutputStream; +import org.bouncycastle.bcpg.ModDetectionCodePacket; +import org.bouncycastle.bcpg.OnePassSignaturePacket; +import org.bouncycastle.bcpg.Packet; +import org.bouncycastle.bcpg.PacketTags; +import org.bouncycastle.bcpg.PublicKeyEncSessionPacket; +import org.bouncycastle.bcpg.SignaturePacket; +import org.bouncycastle.bcpg.SymmetricEncDataPacket; +import org.bouncycastle.bcpg.SymmetricEncIntegrityPacket; +import org.bouncycastle.bcpg.SymmetricKeyEncSessionPacket; +import org.bouncycastle.bcpg.TrustPacket; +import org.bouncycastle.bcpg.UserAttributePacket; +import org.bouncycastle.bcpg.UserIDPacket; +import org.bouncycastle.openpgp.PGPCompressedData; +import org.bouncycastle.openpgp.PGPEncryptedDataList; +import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.PGPLiteralData; +import org.bouncycastle.openpgp.PGPOnePassSignatureList; +import org.bouncycastle.openpgp.PGPSignatureList; +import org.pgpainless.algorithm.OpenPgpPacket; +import org.pgpainless.exception.MalformedOpenPgpMessageException; +import org.pgpainless.implementation.ImplementationFactory; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.NoSuchElementException; +import java.util.Stack; + +public class PGPDecryptionStream extends InputStream { + + PushdownAutomaton automaton = new PushdownAutomaton(); + // nested streams, outermost at the bottom of the stack + Stack packetLayers = new Stack<>(); + + public PGPDecryptionStream(InputStream inputStream) throws IOException, PGPException { + try { + packetLayers.push(Layer.initial(inputStream)); + walkLayer(); + } catch (MalformedOpenPgpMessageException e) { + throw e.toRuntimeException(); + } + } + + private void walkLayer() throws PGPException, IOException { + if (packetLayers.isEmpty()) { + return; + } + + Layer layer = packetLayers.peek(); + BCPGInputStream inputStream = (BCPGInputStream) layer.inputStream; + + loop: while (true) { + if (inputStream.nextPacketTag() == -1) { + popLayer(); + break loop; + } + OpenPgpPacket tag = nextTagOrThrow(inputStream); + switch (tag) { + + case PKESK: + PublicKeyEncSessionPacket pkeskPacket = (PublicKeyEncSessionPacket) inputStream.readPacket(); + PGPEncryptedDataList encList = null; + break; + case SIG: + automaton.next(PushdownAutomaton.InputAlphabet.Signatures); + + ByteArrayOutputStream buf = new ByteArrayOutputStream(); + BCPGOutputStream bcpgOut = new BCPGOutputStream(buf); + while (inputStream.nextPacketTag() == PacketTags.SIGNATURE || inputStream.nextPacketTag() == PacketTags.MARKER) { + Packet packet = inputStream.readPacket(); + if (packet instanceof SignaturePacket) { + SignaturePacket sig = (SignaturePacket) packet; + sig.encode(bcpgOut); + } + } + PGPSignatureList signatures = (PGPSignatureList) ImplementationFactory.getInstance() + .getPGPObjectFactory(buf.toByteArray()).nextObject(); + break; + case SKESK: + SymmetricKeyEncSessionPacket skeskPacket = (SymmetricKeyEncSessionPacket) inputStream.readPacket(); + + break; + case OPS: + automaton.next(PushdownAutomaton.InputAlphabet.OnePassSignatures); + buf = new ByteArrayOutputStream(); + bcpgOut = new BCPGOutputStream(buf); + while (inputStream.nextPacketTag() == PacketTags.ONE_PASS_SIGNATURE || inputStream.nextPacketTag() == PacketTags.MARKER) { + Packet packet = inputStream.readPacket(); + if (packet instanceof OnePassSignaturePacket) { + OnePassSignaturePacket sig = (OnePassSignaturePacket) packet; + sig.encode(bcpgOut); + } + } + PGPOnePassSignatureList onePassSignatures = (PGPOnePassSignatureList) ImplementationFactory.getInstance() + .getPGPObjectFactory(buf.toByteArray()).nextObject(); + break; + case SK: + break; + case PK: + break; + case SSK: + break; + case COMP: + automaton.next(PushdownAutomaton.InputAlphabet.CompressedData); + PGPCompressedData compressedData = new PGPCompressedData(inputStream); + inputStream = new BCPGInputStream(compressedData.getDataStream()); + packetLayers.push(Layer.CompressedData(inputStream)); + break; + case SED: + automaton.next(PushdownAutomaton.InputAlphabet.EncryptedData); + SymmetricEncDataPacket symmetricEncDataPacket = (SymmetricEncDataPacket) inputStream.readPacket(); + break; + case MARKER: + inputStream.readPacket(); // discard + break; + case LIT: + automaton.next(PushdownAutomaton.InputAlphabet.LiteralData); + PGPLiteralData literalData = new PGPLiteralData(inputStream); + packetLayers.push(Layer.LiteralMessage(literalData.getDataStream())); + break loop; + case TRUST: + TrustPacket trustPacket = (TrustPacket) inputStream.readPacket(); + break; + case UID: + UserIDPacket userIDPacket = (UserIDPacket) inputStream.readPacket(); + break; + case PSK: + break; + case UATTR: + UserAttributePacket userAttributePacket = (UserAttributePacket) inputStream.readPacket(); + break; + case SEIPD: + automaton.next(PushdownAutomaton.InputAlphabet.EncryptedData); + SymmetricEncIntegrityPacket symmetricEncIntegrityPacket = (SymmetricEncIntegrityPacket) inputStream.readPacket(); + break; + case MOD: + ModDetectionCodePacket modDetectionCodePacket = (ModDetectionCodePacket) inputStream.readPacket(); + break; + case EXP_1: + break; + case EXP_2: + break; + case EXP_3: + break; + case EXP_4: + break; + } + } + } + + private OpenPgpPacket nextTagOrThrow(BCPGInputStream inputStream) + throws IOException, InvalidOpenPgpPacketException { + try { + return OpenPgpPacket.requireFromTag(inputStream.nextPacketTag()); + } catch (NoSuchElementException e) { + throw new InvalidOpenPgpPacketException(e.getMessage()); + } + } + + private void popLayer() throws MalformedOpenPgpMessageException { + if (packetLayers.pop().isNested) + automaton.next(PushdownAutomaton.InputAlphabet.EndOfSequence); + } + + @Override + public int read() throws IOException { + if (packetLayers.isEmpty()) { + try { + automaton.assertValid(); + } catch (MalformedOpenPgpMessageException e) { + throw e.toRuntimeException(); + } + return -1; + } + + int r = -1; + try { + r = packetLayers.peek().inputStream.read(); + } catch (IOException e) { + } + if (r == -1) { + try { + popLayer(); + walkLayer(); + } catch (MalformedOpenPgpMessageException e) { + throw e.toRuntimeException(); + } + catch (PGPException e) { + throw new RuntimeException(e); + } + return read(); + } + return r; + } + + public static class InvalidOpenPgpPacketException extends PGPException { + + public InvalidOpenPgpPacketException(String message) { + super(message); + } + } + + private static class Layer { + InputStream inputStream; + boolean isNested; + + private Layer(InputStream inputStream, boolean isNested) { + this.inputStream = inputStream; + this.isNested = isNested; + } + + static Layer initial(InputStream inputStream) { + BCPGInputStream bcpgIn; + if (inputStream instanceof BCPGInputStream) { + bcpgIn = (BCPGInputStream) inputStream; + } else { + bcpgIn = new BCPGInputStream(inputStream); + } + return new Layer(bcpgIn, true); + } + + static Layer LiteralMessage(InputStream inputStream) { + return new Layer(inputStream, false); + } + + static Layer CompressedData(InputStream inputStream) { + return new Layer(inputStream, true); + } + } +} diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/PushdownAutomaton.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/PushdownAutomaton.java index 6930de4b..86075280 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/PushdownAutomaton.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/PushdownAutomaton.java @@ -342,6 +342,12 @@ public class PushdownAutomaton { return getState() == State.Valid && stack.isEmpty(); } + public void assertValid() throws MalformedOpenPgpMessageException { + if (!isValid()) { + throw new MalformedOpenPgpMessageException("Pushdown Automaton is not in an acceptable state: " + toString()); + } + } + /** * Pop an item from the stack. * diff --git a/pgpainless-core/src/main/java/org/pgpainless/exception/MalformedOpenPgpMessageException.java b/pgpainless-core/src/main/java/org/pgpainless/exception/MalformedOpenPgpMessageException.java index 07fed365..9ce2284d 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/exception/MalformedOpenPgpMessageException.java +++ b/pgpainless-core/src/main/java/org/pgpainless/exception/MalformedOpenPgpMessageException.java @@ -28,4 +28,15 @@ public class MalformedOpenPgpMessageException extends PGPException { PushdownAutomaton.StackAlphabet stackItem) { this("Invalid input: There is no legal transition from state '" + state + "' for input '" + input + "' when '" + stackItem + "' is on top of the stack."); } + + public RTE toRuntimeException() { + return new RTE(this); + } + + public static class RTE extends RuntimeException { + + public RTE(MalformedOpenPgpMessageException e) { + super(e); + } + } } diff --git a/pgpainless-core/src/test/java/org/pgpainless/algorithm/OpenPgpPacketTest.java b/pgpainless-core/src/test/java/org/pgpainless/algorithm/OpenPgpPacketTest.java new file mode 100644 index 00000000..88146605 --- /dev/null +++ b/pgpainless-core/src/test/java/org/pgpainless/algorithm/OpenPgpPacketTest.java @@ -0,0 +1,38 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.algorithm; + +import org.bouncycastle.bcpg.PacketTags; +import org.junit.jupiter.api.Test; + +import java.util.NoSuchElementException; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; + +public class OpenPgpPacketTest { + + @Test + public void testFromInvalidTag() { + int tag = PacketTags.RESERVED; + assertNull(OpenPgpPacket.fromTag(tag)); + assertThrows(NoSuchElementException.class, + () -> OpenPgpPacket.requireFromTag(tag)); + } + + @Test + public void testFromExistingTags() { + for (OpenPgpPacket p : OpenPgpPacket.values()) { + assertEquals(p, OpenPgpPacket.fromTag(p.getTag())); + assertEquals(p, OpenPgpPacket.requireFromTag(p.getTag())); + } + } + + @Test + public void testPKESKTagMatches() { + assertEquals(PacketTags.PUBLIC_KEY_ENC_SESSION, OpenPgpPacket.PKESK.getTag()); + } +} diff --git a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/PGPDecryptionStreamTest.java b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/PGPDecryptionStreamTest.java new file mode 100644 index 00000000..bb2742b5 --- /dev/null +++ b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/PGPDecryptionStreamTest.java @@ -0,0 +1,290 @@ +package org.pgpainless.decryption_verification; + +import org.bouncycastle.bcpg.ArmoredInputStream; +import org.bouncycastle.bcpg.ArmoredOutputStream; +import org.bouncycastle.bcpg.CompressionAlgorithmTags; +import org.bouncycastle.openpgp.PGPCompressedDataGenerator; +import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.PGPLiteralData; +import org.bouncycastle.openpgp.PGPLiteralDataGenerator; +import org.bouncycastle.openpgp.PGPSecretKeyRing; +import org.bouncycastle.openpgp.PGPSignature; +import org.bouncycastle.util.io.Streams; +import org.junit.jupiter.api.Test; +import org.pgpainless.PGPainless; +import org.pgpainless.encryption_signing.EncryptionResult; +import org.pgpainless.encryption_signing.EncryptionStream; +import org.pgpainless.encryption_signing.ProducerOptions; +import org.pgpainless.encryption_signing.SigningOptions; +import org.pgpainless.exception.MalformedOpenPgpMessageException; +import org.pgpainless.key.protection.SecretKeyRingProtector; +import org.pgpainless.util.ArmoredInputStreamFactory; + +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; + +public class PGPDecryptionStreamTest { + + private static final String KEY = "-----BEGIN PGP PRIVATE KEY BLOCK-----\n" + + "Version: PGPainless\n" + + "Comment: DA05 848F 37D4 68E6 F982 C889 7A70 1FC6 904D 3F4C\n" + + "Comment: Alice \n" + + "\n" + + "lFgEYxzSCBYJKwYBBAHaRw8BAQdAeJU8m4GOJb1eQgv/ryilFHRfNLTYFMNqL6zj\n" + + "r0vF7dsAAP42rAtngpJ6dZxoZlJX0Je65zk1VMPeTrXaWfPS2HSKBRGptBxBbGlj\n" + + "ZSA8YWxpY2VAcGdwYWlubGVzcy5vcmc+iI8EExYKAEEFAmMc0ggJEHpwH8aQTT9M\n" + + "FiEE2gWEjzfUaOb5gsiJenAfxpBNP0wCngECmwEFFgIDAQAECwkIBwUVCgkICwKZ\n" + + "AQAApZEBALUXHtvswPZG28YO+16Men6/fpk+scvqpNMnD4ty3IkAAPwK6TuXjNnZ\n" + + "0XuWdnilvLMV23Ai1d5g6em+lwLK5M2SApxdBGMc0ggSCisGAQQBl1UBBQEBB0D8\n" + + "mNUVX8y2MXFaSeFYqOTPFnGT7dgNVdn6yc0UtkkHOgMBCAcAAP9y9OtP4SX9voPb\n" + + "ID2u9PkJKgo4hTB8NK5LouGppdRtEBGriHUEGBYKAB0FAmMc0ggCngECmwwFFgID\n" + + "AQAECwkIBwUVCgkICwAKCRB6cB/GkE0/TAywAQDpZRJS/joFH4+xcwheqWfI7ay/\n" + + "WfojUoGQMYGnUjsgYwEAkceRUsgkqI0SVgYvuglfaQpZ9k2ns1mZGVLkXvu/yQyc\n" + + "WARjHNIIFgkrBgEEAdpHDwEBB0BGN9BybSOrj8B6gim1SjbB/IiqAshlzMDunVkQ\n" + + "X23npQABAJqvjOOY7qhBuTusC5/Q5+25iLrhMn4TI+LXlJHMVNOaE0OI1QQYFgoA\n" + + "fQUCYxzSCAKeAQKbAgUWAgMBAAQLCQgHBRUKCQgLXyAEGRYKAAYFAmMc0ggACgkQ\n" + + "KALh4BJQXl6yTQD/dh0N5228Uwtu7XHy6dmpMRX62cac5tXQ9WaDzpy8STgBAMdn\n" + + "Mq948UOYEhdk/ZY2/hwux/4t+FHvqrXW8ziBe4cLAAoJEHpwH8aQTT9M1hQA/3Ms\n" + + "P3kzoed3VsWu1ZMr7dKEngbc6SoJ2XPayzN0QYJaAQCIY5NcT9mZF97HWV3Vgeum\n" + + "00sWMHXfkW3+nl5OpUZaDA==\n" + + "=THgv\n" + + "-----END PGP PRIVATE KEY BLOCK-----"; + + private static final String PLAINTEXT = "Hello, World!\n"; + + private static final String LIT = "" + + "-----BEGIN PGP MESSAGE-----\n" + + "Version: PGPainless\n" + + "\n" + + "yxRiAAAAAABIZWxsbywgV29ybGQhCg==\n" + + "=WGju\n" + + "-----END PGP MESSAGE-----"; + + private static final String LIT_LIT = "" + + "-----BEGIN PGP MESSAGE-----\n" + + "Version: BCPG v1.71\n" + + "\n" + + "yxRiAAAAAABIZWxsbywgV29ybGQhCssUYgAAAAAASGVsbG8sIFdvcmxkIQo=\n" + + "=A91Q\n" + + "-----END PGP MESSAGE-----"; + + private static final String COMP_LIT = "" + + "-----BEGIN PGP MESSAGE-----\n" + + "Version: BCPG v1.71\n" + + "\n" + + "owE7LZLEAAIeqTk5+ToK4flFOSmKXAA=\n" + + "=ZYDg\n" + + "-----END PGP MESSAGE-----"; + + private static final String COMP = "" + + "-----BEGIN PGP MESSAGE-----\n" + + "Version: BCPG v1.71\n" + + "\n" + + "owEDAA==\n" + + "=MDzg\n" + + "-----END PGP MESSAGE-----"; + + private static final String COMP_COMP_LIT = "" + + "-----BEGIN PGP MESSAGE-----\n" + + "Version: BCPG v1.71\n" + + "\n" + + "owEBRwC4/6MDQlpoOTFBWSZTWVuW2KAAAAr3hGAQBABgBABAAIAWBJAAAAggADFM\n" + + "ABNBqBo00N6puqWR+TqInoXQ58XckU4UJBbltigA\n" + + "=K9Zl\n" + + "-----END PGP MESSAGE-----"; + + private static final String SIG_LIT = "" + + "-----BEGIN PGP MESSAGE-----\n" + + "Version: BCPG v1.71\n" + + "\n" + + "iHUEABYKACcFAmMc1i0JECgC4eASUF5eFiEEjN3RiJxCf/TyYOQjKALh4BJQXl4A\n" + + "AHkrAP98uPpqrgIix7epgL7MM1cjXXGSxqbDfXHwgptk1YGQlgD/fw89VGcXwFaI\n" + + "2k7kpXQYy/1BqnovM/jZ3X3mXhhTaAOjATstksQAAh6pOTn5Ogrh+UU5KYpcAA==\n" + + "=WKPn\n" + + "-----END PGP MESSAGE-----"; + + @Test + public void genLIT() throws IOException { + ArmoredOutputStream armorOut = new ArmoredOutputStream(System.out); + PGPLiteralDataGenerator litGen = new PGPLiteralDataGenerator(); + OutputStream litOut = litGen.open(armorOut, PGPLiteralDataGenerator.BINARY, "", PGPLiteralData.NOW, new byte[1 << 9]); + litOut.write(PLAINTEXT.getBytes(StandardCharsets.UTF_8)); + litOut.close(); + armorOut.close(); + } + + @Test + public void processLIT() throws IOException, PGPException { + ByteArrayInputStream bytesIn = new ByteArrayInputStream(LIT.getBytes(StandardCharsets.UTF_8)); + ArmoredInputStream armorIn = ArmoredInputStreamFactory.get(bytesIn); + PGPDecryptionStream decIn = new PGPDecryptionStream(armorIn); + + ByteArrayOutputStream out = new ByteArrayOutputStream(); + Streams.pipeAll(decIn, out); + assertEquals(PLAINTEXT, out.toString()); + armorIn.close(); + } + + @Test + public void getLIT_LIT() throws IOException { + ArmoredOutputStream armorOut = new ArmoredOutputStream(System.out); + PGPLiteralDataGenerator litGen = new PGPLiteralDataGenerator(); + OutputStream litOut = litGen.open(armorOut, PGPLiteralDataGenerator.BINARY, "", PGPLiteralData.NOW, new byte[1 << 9]); + litOut.write(PLAINTEXT.getBytes(StandardCharsets.UTF_8)); + litOut.close(); + + litOut = litGen.open(armorOut, PGPLiteralDataGenerator.BINARY, "", PGPLiteralData.NOW, new byte[1 << 9]); + litOut.write(PLAINTEXT.getBytes(StandardCharsets.UTF_8)); + litOut.close(); + + armorOut.close(); + } + + @Test + public void processLIT_LIT() throws IOException, PGPException { + ByteArrayInputStream bytesIn = new ByteArrayInputStream(LIT_LIT.getBytes(StandardCharsets.UTF_8)); + ArmoredInputStream armorIn = ArmoredInputStreamFactory.get(bytesIn); + PGPDecryptionStream decIn = new PGPDecryptionStream(armorIn); + + ByteArrayOutputStream out = new ByteArrayOutputStream(); + assertThrows(MalformedOpenPgpMessageException.RTE.class, () -> Streams.pipeAll(decIn, out)); + } + + @Test + public void genCOMP_LIT() throws IOException { + ArmoredOutputStream armorOut = new ArmoredOutputStream(System.out); + PGPCompressedDataGenerator compGen = new PGPCompressedDataGenerator(CompressionAlgorithmTags.ZIP); + OutputStream compOut = compGen.open(armorOut); + PGPLiteralDataGenerator litGen = new PGPLiteralDataGenerator(); + OutputStream litOut = litGen.open(compOut, PGPLiteralDataGenerator.BINARY, "", PGPLiteralData.NOW, new byte[1 << 9]); + litOut.write(PLAINTEXT.getBytes(StandardCharsets.UTF_8)); + litOut.close(); + compOut.close(); + armorOut.close(); + } + + @Test + public void processCOMP_LIT() throws IOException, PGPException { + ByteArrayInputStream bytesIn = new ByteArrayInputStream(COMP_LIT.getBytes(StandardCharsets.UTF_8)); + ArmoredInputStream armorIn = ArmoredInputStreamFactory.get(bytesIn); + PGPDecryptionStream decIn = new PGPDecryptionStream(armorIn); + + ByteArrayOutputStream out = new ByteArrayOutputStream(); + Streams.pipeAll(decIn, out); + decIn.close(); + armorIn.close(); + + assertEquals(PLAINTEXT, out.toString()); + } + + @Test + public void genCOMP() throws IOException { + ArmoredOutputStream armorOut = new ArmoredOutputStream(System.out); + PGPCompressedDataGenerator compGen = new PGPCompressedDataGenerator(CompressionAlgorithmTags.ZIP); + OutputStream compOut = compGen.open(armorOut); + compOut.close(); + armorOut.close(); + } + + @Test + public void processCOMP() throws IOException { + ByteArrayInputStream bytesIn = new ByteArrayInputStream(COMP.getBytes(StandardCharsets.UTF_8)); + ArmoredInputStream armorIn = ArmoredInputStreamFactory.get(bytesIn); + assertThrows(MalformedOpenPgpMessageException.RTE.class, () -> { + PGPDecryptionStream decIn = new PGPDecryptionStream(armorIn); + Streams.drain(decIn); + }); + } + + @Test + public void genCOMP_COMP_LIT() throws IOException { + ArmoredOutputStream armorOut = new ArmoredOutputStream(System.out); + + PGPCompressedDataGenerator compGen1 = new PGPCompressedDataGenerator(CompressionAlgorithmTags.ZIP); + OutputStream compOut1 = compGen1.open(armorOut); + + PGPCompressedDataGenerator compGen2 = new PGPCompressedDataGenerator(CompressionAlgorithmTags.BZIP2); + OutputStream compOut2 = compGen2.open(compOut1); + + PGPLiteralDataGenerator litGen = new PGPLiteralDataGenerator(); + OutputStream litOut = litGen.open(compOut2, PGPLiteralDataGenerator.BINARY, "", PGPLiteralDataGenerator.NOW, new byte[1 << 9]); + + litOut.write(PLAINTEXT.getBytes(StandardCharsets.UTF_8)); + litOut.close(); + compOut2.close(); + compOut1.close(); + armorOut.close(); + } + + @Test + public void processCOMP_COMP_LIT() throws PGPException, IOException { + ByteArrayInputStream bytesIn = new ByteArrayInputStream(COMP_COMP_LIT.getBytes(StandardCharsets.UTF_8)); + ArmoredInputStream armorIn = ArmoredInputStreamFactory.get(bytesIn); + PGPDecryptionStream decIn = new PGPDecryptionStream(armorIn); + + ByteArrayOutputStream out = new ByteArrayOutputStream(); + Streams.pipeAll(decIn, out); + decIn.close(); + + assertEquals(PLAINTEXT, out.toString()); + } + + @Test + public void genKey() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, IOException { + System.out.println(PGPainless.asciiArmor( + PGPainless.generateKeyRing().modernKeyRing("Alice ") + )); + } + + @Test + public void genSIG_LIT() throws PGPException, IOException { + PGPSecretKeyRing secretKeys = PGPainless.readKeyRing().secretKeyRing(KEY); + ByteArrayOutputStream msgOut = new ByteArrayOutputStream(); + EncryptionStream signer = PGPainless.encryptAndOrSign() + .onOutputStream(msgOut) + .withOptions( + ProducerOptions.sign( + SigningOptions.get() + .addDetachedSignature(SecretKeyRingProtector.unprotectedKeys(), secretKeys) + ).setAsciiArmor(false) + ); + + Streams.pipeAll(new ByteArrayInputStream(PLAINTEXT.getBytes(StandardCharsets.UTF_8)), signer); + signer.close(); + EncryptionResult result = signer.getResult(); + PGPSignature detachedSignature = result.getDetachedSignatures().get(result.getDetachedSignatures().keySet().iterator().next()).iterator().next(); + + ByteArrayOutputStream out = new ByteArrayOutputStream(); + ArmoredOutputStream armorOut = new ArmoredOutputStream(out); + armorOut.flush(); + detachedSignature.encode(armorOut); + armorOut.write(msgOut.toByteArray()); + armorOut.close(); + + String armored = out.toString(); + System.out.println(armored + .replace("-----BEGIN PGP SIGNATURE-----\n", "-----BEGIN PGP MESSAGE-----\n") + .replace("-----END PGP SIGNATURE-----", "-----END PGP MESSAGE-----")); + } + + @Test + public void processSIG_LIT() throws IOException, PGPException { + ByteArrayInputStream bytesIn = new ByteArrayInputStream(SIG_LIT.getBytes(StandardCharsets.UTF_8)); + ArmoredInputStream armorIn = ArmoredInputStreamFactory.get(bytesIn); + PGPDecryptionStream decIn = new PGPDecryptionStream(armorIn); + + ByteArrayOutputStream out = new ByteArrayOutputStream(); + Streams.pipeAll(decIn, out); + decIn.close(); + + System.out.println(out); + } +} From 6233ac61e68ed6a8fca8c7f45df067171f7b7548 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 13 Sep 2022 19:23:59 +0200 Subject: [PATCH 03/80] WIP: Replace nesting with independent instancing --- ...ream.java => MessageDecryptionStream.java} | 210 ++++++----- .../OpenPgpMessageInputStream.java | 331 ++++++++++++++++++ .../automaton/InputAlphabet.java | 41 +++ .../NestingPDA.java} | 90 +---- .../automaton/PDA.java | 237 +++++++++++++ .../automaton/StackAlphabet.java | 20 ++ .../MalformedOpenPgpMessageException.java | 25 +- .../org/pgpainless/key/info/KeyRingInfo.java | 40 ++- .../OpenPgpMessageInputStreamTest.java | 86 +++++ .../PGPDecryptionStreamTest.java | 73 +++- .../PushDownAutomatonTest.java | 205 ----------- .../automaton/NestingPDATest.java | 205 +++++++++++ .../automaton/PDATest.java | 75 ++++ 13 files changed, 1227 insertions(+), 411 deletions(-) rename pgpainless-core/src/main/java/org/pgpainless/decryption_verification/{PGPDecryptionStream.java => MessageDecryptionStream.java} (59%) create mode 100644 pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java create mode 100644 pgpainless-core/src/main/java/org/pgpainless/decryption_verification/automaton/InputAlphabet.java rename pgpainless-core/src/main/java/org/pgpainless/decryption_verification/{PushdownAutomaton.java => automaton/NestingPDA.java} (78%) create mode 100644 pgpainless-core/src/main/java/org/pgpainless/decryption_verification/automaton/PDA.java create mode 100644 pgpainless-core/src/main/java/org/pgpainless/decryption_verification/automaton/StackAlphabet.java create mode 100644 pgpainless-core/src/test/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStreamTest.java delete mode 100644 pgpainless-core/src/test/java/org/pgpainless/decryption_verification/PushDownAutomatonTest.java create mode 100644 pgpainless-core/src/test/java/org/pgpainless/decryption_verification/automaton/NestingPDATest.java create mode 100644 pgpainless-core/src/test/java/org/pgpainless/decryption_verification/automaton/PDATest.java diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/PGPDecryptionStream.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/MessageDecryptionStream.java similarity index 59% rename from pgpainless-core/src/main/java/org/pgpainless/decryption_verification/PGPDecryptionStream.java rename to pgpainless-core/src/main/java/org/pgpainless/decryption_verification/MessageDecryptionStream.java index c3053555..335e9d57 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/PGPDecryptionStream.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/MessageDecryptionStream.java @@ -12,41 +12,50 @@ import org.bouncycastle.bcpg.Packet; import org.bouncycastle.bcpg.PacketTags; import org.bouncycastle.bcpg.PublicKeyEncSessionPacket; import org.bouncycastle.bcpg.SignaturePacket; -import org.bouncycastle.bcpg.SymmetricEncDataPacket; -import org.bouncycastle.bcpg.SymmetricEncIntegrityPacket; import org.bouncycastle.bcpg.SymmetricKeyEncSessionPacket; -import org.bouncycastle.bcpg.TrustPacket; -import org.bouncycastle.bcpg.UserAttributePacket; -import org.bouncycastle.bcpg.UserIDPacket; import org.bouncycastle.openpgp.PGPCompressedData; +import org.bouncycastle.openpgp.PGPEncryptedData; import org.bouncycastle.openpgp.PGPEncryptedDataList; import org.bouncycastle.openpgp.PGPException; import org.bouncycastle.openpgp.PGPLiteralData; import org.bouncycastle.openpgp.PGPOnePassSignatureList; +import org.bouncycastle.openpgp.PGPPBEEncryptedData; import org.bouncycastle.openpgp.PGPSignatureList; +import org.bouncycastle.openpgp.operator.PBEDataDecryptorFactory; import org.pgpainless.algorithm.OpenPgpPacket; +import org.pgpainless.decryption_verification.automaton.InputAlphabet; +import org.pgpainless.decryption_verification.automaton.NestingPDA; import org.pgpainless.exception.MalformedOpenPgpMessageException; +import org.pgpainless.exception.MessageNotIntegrityProtectedException; +import org.pgpainless.exception.MissingDecryptionMethodException; import org.pgpainless.implementation.ImplementationFactory; +import org.pgpainless.util.Passphrase; +import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; +import java.io.SequenceInputStream; +import java.util.ArrayList; +import java.util.List; import java.util.NoSuchElementException; import java.util.Stack; -public class PGPDecryptionStream extends InputStream { +public class MessageDecryptionStream extends InputStream { - PushdownAutomaton automaton = new PushdownAutomaton(); + private final ConsumerOptions options; + + NestingPDA automaton = new NestingPDA(); // nested streams, outermost at the bottom of the stack Stack packetLayers = new Stack<>(); + List pkeskList = new ArrayList<>(); + List skeskList = new ArrayList<>(); - public PGPDecryptionStream(InputStream inputStream) throws IOException, PGPException { - try { - packetLayers.push(Layer.initial(inputStream)); - walkLayer(); - } catch (MalformedOpenPgpMessageException e) { - throw e.toRuntimeException(); - } + public MessageDecryptionStream(InputStream inputStream, ConsumerOptions options) + throws IOException, PGPException { + this.options = options; + packetLayers.push(Layer.initial(inputStream)); + walkLayer(); } private void walkLayer() throws PGPException, IOException { @@ -54,6 +63,7 @@ public class PGPDecryptionStream extends InputStream { return; } + // We are currently in the deepest layer Layer layer = packetLayers.peek(); BCPGInputStream inputStream = (BCPGInputStream) layer.inputStream; @@ -65,33 +75,23 @@ public class PGPDecryptionStream extends InputStream { OpenPgpPacket tag = nextTagOrThrow(inputStream); switch (tag) { - case PKESK: - PublicKeyEncSessionPacket pkeskPacket = (PublicKeyEncSessionPacket) inputStream.readPacket(); - PGPEncryptedDataList encList = null; - break; - case SIG: - automaton.next(PushdownAutomaton.InputAlphabet.Signatures); + case LIT: + automaton.next(InputAlphabet.LiteralData); + PGPLiteralData literalData = new PGPLiteralData(inputStream); + packetLayers.push(Layer.literalMessage(literalData.getDataStream())); + break loop; + case COMP: + automaton.next(InputAlphabet.CompressedData); + PGPCompressedData compressedData = new PGPCompressedData(inputStream); + inputStream = new BCPGInputStream(compressedData.getDataStream()); + packetLayers.push(Layer.compressedData(inputStream)); + break; + + case OPS: + automaton.next(InputAlphabet.OnePassSignatures); ByteArrayOutputStream buf = new ByteArrayOutputStream(); BCPGOutputStream bcpgOut = new BCPGOutputStream(buf); - while (inputStream.nextPacketTag() == PacketTags.SIGNATURE || inputStream.nextPacketTag() == PacketTags.MARKER) { - Packet packet = inputStream.readPacket(); - if (packet instanceof SignaturePacket) { - SignaturePacket sig = (SignaturePacket) packet; - sig.encode(bcpgOut); - } - } - PGPSignatureList signatures = (PGPSignatureList) ImplementationFactory.getInstance() - .getPGPObjectFactory(buf.toByteArray()).nextObject(); - break; - case SKESK: - SymmetricKeyEncSessionPacket skeskPacket = (SymmetricKeyEncSessionPacket) inputStream.readPacket(); - - break; - case OPS: - automaton.next(PushdownAutomaton.InputAlphabet.OnePassSignatures); - buf = new ByteArrayOutputStream(); - bcpgOut = new BCPGOutputStream(buf); while (inputStream.nextPacketTag() == PacketTags.ONE_PASS_SIGNATURE || inputStream.nextPacketTag() == PacketTags.MARKER) { Packet packet = inputStream.readPacket(); if (packet instanceof OnePassSignaturePacket) { @@ -102,60 +102,103 @@ public class PGPDecryptionStream extends InputStream { PGPOnePassSignatureList onePassSignatures = (PGPOnePassSignatureList) ImplementationFactory.getInstance() .getPGPObjectFactory(buf.toByteArray()).nextObject(); break; - case SK: + + case SIG: + automaton.next(InputAlphabet.Signatures); + + buf = new ByteArrayOutputStream(); + bcpgOut = new BCPGOutputStream(buf); + while (inputStream.nextPacketTag() == PacketTags.SIGNATURE || inputStream.nextPacketTag() == PacketTags.MARKER) { + Packet packet = inputStream.readPacket(); + if (packet instanceof SignaturePacket) { + SignaturePacket sig = (SignaturePacket) packet; + sig.encode(bcpgOut); + } + } + PGPSignatureList signatures = (PGPSignatureList) ImplementationFactory.getInstance() + .getPGPObjectFactory(buf.toByteArray()).nextObject(); break; - case PK: + + case PKESK: + PublicKeyEncSessionPacket pkeskPacket = (PublicKeyEncSessionPacket) inputStream.readPacket(); + pkeskList.add(pkeskPacket); break; - case SSK: - break; - case COMP: - automaton.next(PushdownAutomaton.InputAlphabet.CompressedData); - PGPCompressedData compressedData = new PGPCompressedData(inputStream); - inputStream = new BCPGInputStream(compressedData.getDataStream()); - packetLayers.push(Layer.CompressedData(inputStream)); + + case SKESK: + SymmetricKeyEncSessionPacket skeskPacket = (SymmetricKeyEncSessionPacket) inputStream.readPacket(); + skeskList.add(skeskPacket); break; + case SED: - automaton.next(PushdownAutomaton.InputAlphabet.EncryptedData); - SymmetricEncDataPacket symmetricEncDataPacket = (SymmetricEncDataPacket) inputStream.readPacket(); - break; + if (!options.isIgnoreMDCErrors()) { + throw new MessageNotIntegrityProtectedException(); + } + // No break; we continue below! + case SEIPD: + automaton.next(InputAlphabet.EncryptedData); + PGPEncryptedDataList encryptedDataList = assembleEncryptedDataList(inputStream); + + for (PGPEncryptedData encData : encryptedDataList) { + if (encData instanceof PGPPBEEncryptedData) { + PGPPBEEncryptedData skenc = (PGPPBEEncryptedData) encData; + for (Passphrase passphrase : options.getDecryptionPassphrases()) { + PBEDataDecryptorFactory decryptorFactory = ImplementationFactory.getInstance() + .getPBEDataDecryptorFactory(passphrase); + InputStream decryptedIn = skenc.getDataStream(decryptorFactory); + packetLayers.push(Layer.encryptedData(new BCPGInputStream(decryptedIn))); + walkLayer(); + break loop; + } + } + } + throw new MissingDecryptionMethodException("Cannot decrypt message."); + case MARKER: inputStream.readPacket(); // discard break; - case LIT: - automaton.next(PushdownAutomaton.InputAlphabet.LiteralData); - PGPLiteralData literalData = new PGPLiteralData(inputStream); - packetLayers.push(Layer.LiteralMessage(literalData.getDataStream())); - break loop; - case TRUST: - TrustPacket trustPacket = (TrustPacket) inputStream.readPacket(); - break; - case UID: - UserIDPacket userIDPacket = (UserIDPacket) inputStream.readPacket(); - break; + + case SK: + case PK: + case SSK: case PSK: - break; + case TRUST: + case UID: case UATTR: - UserAttributePacket userAttributePacket = (UserAttributePacket) inputStream.readPacket(); - break; - case SEIPD: - automaton.next(PushdownAutomaton.InputAlphabet.EncryptedData); - SymmetricEncIntegrityPacket symmetricEncIntegrityPacket = (SymmetricEncIntegrityPacket) inputStream.readPacket(); - break; + throw new MalformedOpenPgpMessageException("OpenPGP packet " + tag + " MUST NOT be part of OpenPGP messages."); case MOD: ModDetectionCodePacket modDetectionCodePacket = (ModDetectionCodePacket) inputStream.readPacket(); break; case EXP_1: - break; case EXP_2: - break; case EXP_3: - break; case EXP_4: - break; + throw new MalformedOpenPgpMessageException("Experimental packet " + tag + " found inside the message."); } } } + private PGPEncryptedDataList assembleEncryptedDataList(BCPGInputStream inputStream) + throws IOException { + ByteArrayOutputStream buf = new ByteArrayOutputStream(); + BCPGOutputStream bcpgOut = new BCPGOutputStream(buf); + + for (SymmetricKeyEncSessionPacket skesk : skeskList) { + bcpgOut.write(skesk.getEncoded()); + } + skeskList.clear(); + for (PublicKeyEncSessionPacket pkesk : pkeskList) { + bcpgOut.write(pkesk.getEncoded()); + } + pkeskList.clear(); + + SequenceInputStream sqin = new SequenceInputStream( + new ByteArrayInputStream(buf.toByteArray()), inputStream); + + PGPEncryptedDataList encryptedDataList = (PGPEncryptedDataList) ImplementationFactory.getInstance() + .getPGPObjectFactory(sqin).nextObject(); + return encryptedDataList; + } + private OpenPgpPacket nextTagOrThrow(BCPGInputStream inputStream) throws IOException, InvalidOpenPgpPacketException { try { @@ -167,17 +210,13 @@ public class PGPDecryptionStream extends InputStream { private void popLayer() throws MalformedOpenPgpMessageException { if (packetLayers.pop().isNested) - automaton.next(PushdownAutomaton.InputAlphabet.EndOfSequence); + automaton.next(InputAlphabet.EndOfSequence); } @Override public int read() throws IOException { if (packetLayers.isEmpty()) { - try { - automaton.assertValid(); - } catch (MalformedOpenPgpMessageException e) { - throw e.toRuntimeException(); - } + automaton.assertValid(); return -1; } @@ -187,13 +226,10 @@ public class PGPDecryptionStream extends InputStream { } catch (IOException e) { } if (r == -1) { + popLayer(); try { - popLayer(); walkLayer(); - } catch (MalformedOpenPgpMessageException e) { - throw e.toRuntimeException(); - } - catch (PGPException e) { + } catch (PGPException e) { throw new RuntimeException(e); } return read(); @@ -227,11 +263,15 @@ public class PGPDecryptionStream extends InputStream { return new Layer(bcpgIn, true); } - static Layer LiteralMessage(InputStream inputStream) { + static Layer literalMessage(InputStream inputStream) { return new Layer(inputStream, false); } - static Layer CompressedData(InputStream inputStream) { + static Layer compressedData(InputStream inputStream) { + return new Layer(inputStream, true); + } + + static Layer encryptedData(InputStream inputStream) { return new Layer(inputStream, true); } } 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 new file mode 100644 index 00000000..549fe46b --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java @@ -0,0 +1,331 @@ +package org.pgpainless.decryption_verification; + +import com.sun.tools.javac.code.Attribute; +import org.bouncycastle.bcpg.BCPGInputStream; +import org.bouncycastle.bcpg.BCPGOutputStream; +import org.bouncycastle.bcpg.OnePassSignaturePacket; +import org.bouncycastle.bcpg.Packet; +import org.bouncycastle.bcpg.PacketTags; +import org.bouncycastle.bcpg.SignaturePacket; +import org.bouncycastle.openpgp.PGPCompressedData; +import org.bouncycastle.openpgp.PGPEncryptedData; +import org.bouncycastle.openpgp.PGPEncryptedDataList; +import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.PGPLiteralData; +import org.bouncycastle.openpgp.PGPObjectFactory; +import org.bouncycastle.openpgp.PGPOnePassSignature; +import org.bouncycastle.openpgp.PGPOnePassSignatureList; +import org.bouncycastle.openpgp.PGPPBEEncryptedData; +import org.bouncycastle.openpgp.PGPPrivateKey; +import org.bouncycastle.openpgp.PGPPublicKey; +import org.bouncycastle.openpgp.PGPPublicKeyEncryptedData; +import org.bouncycastle.openpgp.PGPSecretKey; +import org.bouncycastle.openpgp.PGPSecretKeyRing; +import org.bouncycastle.openpgp.PGPSignature; +import org.bouncycastle.openpgp.PGPSignatureList; +import org.bouncycastle.openpgp.operator.PBEDataDecryptorFactory; +import org.bouncycastle.openpgp.operator.PublicKeyDataDecryptorFactory; +import org.pgpainless.PGPainless; +import org.pgpainless.algorithm.EncryptionPurpose; +import org.pgpainless.algorithm.OpenPgpPacket; +import org.pgpainless.decryption_verification.automaton.InputAlphabet; +import org.pgpainless.decryption_verification.automaton.PDA; +import org.pgpainless.exception.MalformedOpenPgpMessageException; +import org.pgpainless.exception.MessageNotIntegrityProtectedException; +import org.pgpainless.implementation.ImplementationFactory; +import org.pgpainless.key.info.KeyRingInfo; +import org.pgpainless.key.protection.SecretKeyRingProtector; +import org.pgpainless.key.protection.UnlockSecretKey; +import org.pgpainless.util.Passphrase; +import org.pgpainless.util.Tuple; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.List; + +public class OpenPgpMessageInputStream extends InputStream { + + protected final PDA automaton = new PDA(); + protected final ConsumerOptions options; + protected final BCPGInputStream bcpgIn; + protected InputStream in; + + private List signatures = new ArrayList<>(); + private List onePassSignatures = new ArrayList<>(); + + public OpenPgpMessageInputStream(InputStream inputStream, ConsumerOptions options) + throws IOException, PGPException { + this.options = options; + // TODO: Use BCPGInputStream.wrap(inputStream); + if (inputStream instanceof BCPGInputStream) { + this.bcpgIn = (BCPGInputStream) inputStream; + } else { + this.bcpgIn = new BCPGInputStream(inputStream); + } + + walk(); + } + + private void walk() throws IOException, PGPException { + loop: while (true) { + + int tag = bcpgIn.nextPacketTag(); + if (tag == -1) { + break loop; + } + + OpenPgpPacket nextPacket = OpenPgpPacket.requireFromTag(tag); + switch (nextPacket) { + case LIT: + automaton.next(InputAlphabet.LiteralData); + PGPLiteralData literalData = new PGPLiteralData(bcpgIn); + in = literalData.getDataStream(); + break loop; + + case COMP: + automaton.next(InputAlphabet.CompressedData); + PGPCompressedData compressedData = new PGPCompressedData(bcpgIn); + in = new OpenPgpMessageInputStream(compressedData.getDataStream(), options); + break loop; + + case OPS: + automaton.next(InputAlphabet.OnePassSignatures); + readOnePassSignatures(); + break; + + case SIG: + automaton.next(InputAlphabet.Signatures); + readSignatures(); + break; + + case PKESK: + case SKESK: + case SED: + case SEIPD: + automaton.next(InputAlphabet.EncryptedData); + PGPEncryptedDataList encDataList = new PGPEncryptedDataList(bcpgIn); + + // TODO: Replace with !encDataList.isIntegrityProtected() + if (!encDataList.get(0).isIntegrityProtected()) { + throw new MessageNotIntegrityProtectedException(); + } + + SortedESKs esks = new SortedESKs(encDataList); + + // TODO: try session keys + + // Try passwords + for (PGPPBEEncryptedData skesk : esks.skesks) { + for (Passphrase passphrase : options.getDecryptionPassphrases()) { + PBEDataDecryptorFactory decryptorFactory = ImplementationFactory.getInstance() + .getPBEDataDecryptorFactory(passphrase); + try { + InputStream decrypted = skesk.getDataStream(decryptorFactory); + in = new OpenPgpMessageInputStream(decrypted, options); + break loop; + } catch (PGPException e) { + // password mismatch? Try next password + } + + } + } + + // Try (known) secret keys + for (PGPPublicKeyEncryptedData pkesk : esks.pkesks) { + long keyId = pkesk.getKeyID(); + PGPSecretKeyRing decryptionKeys = getDecryptionKey(keyId); + if (decryptionKeys == null) { + continue; + } + SecretKeyRingProtector protector = options.getSecretKeyProtector(decryptionKeys); + PGPSecretKey decryptionKey = decryptionKeys.getSecretKey(keyId); + PGPPrivateKey privateKey = UnlockSecretKey.unlockSecretKey(decryptionKey, protector); + + PublicKeyDataDecryptorFactory decryptorFactory = ImplementationFactory.getInstance() + .getPublicKeyDataDecryptorFactory(privateKey); + try { + InputStream decrypted = pkesk.getDataStream(decryptorFactory); + in = new OpenPgpMessageInputStream(decrypted, options); + break loop; + } catch (PGPException e) { + // hm :/ + } + } + + // try anonymous secret keys + for (PGPPublicKeyEncryptedData pkesk : esks.anonPkesks) { + for (Tuple decryptionKeyCandidate : findPotentialDecryptionKeys(pkesk)) { + SecretKeyRingProtector protector = options.getSecretKeyProtector(decryptionKeyCandidate.getA()); + PGPPrivateKey privateKey = UnlockSecretKey.unlockSecretKey(decryptionKeyCandidate.getB(), protector); + PublicKeyDataDecryptorFactory decryptorFactory = ImplementationFactory.getInstance() + .getPublicKeyDataDecryptorFactory(privateKey); + + try { + InputStream decrypted = pkesk.getDataStream(decryptorFactory); + in = new OpenPgpMessageInputStream(decrypted, options); + break loop; + } catch (PGPException e) { + // hm :/ + } + } + } + + // TODO: try interactive password callbacks + + break loop; + + case MARKER: + bcpgIn.readPacket(); // skip marker packet + break; + + case SK: + case PK: + case SSK: + case PSK: + case TRUST: + case UID: + case UATTR: + + case MOD: + break; + + case EXP_1: + case EXP_2: + case EXP_3: + case EXP_4: + break; + } + } + } + + private List> findPotentialDecryptionKeys(PGPPublicKeyEncryptedData pkesk) { + int algorithm = pkesk.getAlgorithm(); + List> decryptionKeyCandidates = new ArrayList<>(); + + for (PGPSecretKeyRing secretKeys : options.getDecryptionKeys()) { + KeyRingInfo info = PGPainless.inspectKeyRing(secretKeys); + for (PGPPublicKey publicKey : info.getEncryptionSubkeys(EncryptionPurpose.ANY)) { + if (publicKey.getAlgorithm() == algorithm && info.isSecretKeyAvailable(publicKey.getKeyID())) { + PGPSecretKey candidate = secretKeys.getSecretKey(publicKey.getKeyID()); + decryptionKeyCandidates.add(new Tuple<>(secretKeys, candidate)); + } + } + } + return decryptionKeyCandidates; + } + + private PGPSecretKeyRing getDecryptionKey(long keyID) { + for (PGPSecretKeyRing secretKeys : options.getDecryptionKeys()) { + PGPSecretKey decryptionKey = secretKeys.getSecretKey(keyID); + if (decryptionKey == null) { + continue; + } + return secretKeys; + } + return null; + } + + private void readOnePassSignatures() throws IOException { + ByteArrayOutputStream buf = new ByteArrayOutputStream(); + BCPGOutputStream bcpgOut = new BCPGOutputStream(buf); + int tag = bcpgIn.nextPacketTag(); + while (tag == PacketTags.ONE_PASS_SIGNATURE || tag == PacketTags.MARKER) { + Packet packet = bcpgIn.readPacket(); + if (tag == PacketTags.ONE_PASS_SIGNATURE) { + OnePassSignaturePacket sigPacket = (OnePassSignaturePacket) packet; + sigPacket.encode(bcpgOut); + } + } + bcpgOut.close(); + + PGPObjectFactory objectFactory = ImplementationFactory.getInstance().getPGPObjectFactory(buf.toByteArray()); + PGPOnePassSignatureList signatureList = (PGPOnePassSignatureList) objectFactory.nextObject(); + for (PGPOnePassSignature ops : signatureList) { + onePassSignatures.add(ops); + } + } + + private void readSignatures() throws IOException { + ByteArrayOutputStream buf = new ByteArrayOutputStream(); + BCPGOutputStream bcpgOut = new BCPGOutputStream(buf); + int tag = bcpgIn.nextPacketTag(); + while (tag == PacketTags.SIGNATURE || tag == PacketTags.MARKER) { + Packet packet = bcpgIn.readPacket(); + if (tag == PacketTags.SIGNATURE) { + SignaturePacket sigPacket = (SignaturePacket) packet; + sigPacket.encode(bcpgOut); + } + } + bcpgOut.close(); + + PGPObjectFactory objectFactory = ImplementationFactory.getInstance().getPGPObjectFactory(buf.toByteArray()); + PGPSignatureList signatureList = (PGPSignatureList) objectFactory.nextObject(); + for (PGPSignature signature : signatureList) { + signatures.add(signature); + } + } + + @Override + public int read() throws IOException { + int r = -1; + try { + r = in.read(); + } catch (IOException e) { + // + } + if (r == -1) { + if (in instanceof OpenPgpMessageInputStream) { + in.close(); + } else { + try { + walk(); + } catch (PGPException e) { + throw new RuntimeException(e); + } + } + } + return r; + } + + @Override + public void close() throws IOException { + try { + in.close(); + // Nested streams (except LiteralData) need to be closed. + if (automaton.getState() != PDA.State.LiteralMessage) { + automaton.next(InputAlphabet.EndOfSequence); + automaton.assertValid(); + } + } catch (IOException e) { + // + } + + super.close(); + } + + private static class SortedESKs { + + private List skesks = new ArrayList<>(); + private List pkesks = new ArrayList<>(); + private List anonPkesks = new ArrayList<>(); + + SortedESKs(PGPEncryptedDataList esks) { + for (PGPEncryptedData esk : esks) { + if (esk instanceof PGPPBEEncryptedData) { + skesks.add((PGPPBEEncryptedData) esk); + } else if (esk instanceof PGPPublicKeyEncryptedData) { + PGPPublicKeyEncryptedData pkesk = (PGPPublicKeyEncryptedData) esk; + if (pkesk.getKeyID() != 0) { + pkesks.add(pkesk); + } else { + anonPkesks.add(pkesk); + } + } else { + throw new IllegalArgumentException("Unknown ESK class type."); + } + } + } + } +} diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/automaton/InputAlphabet.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/automaton/InputAlphabet.java new file mode 100644 index 00000000..d015a4b3 --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/automaton/InputAlphabet.java @@ -0,0 +1,41 @@ +package org.pgpainless.decryption_verification.automaton; + +import org.bouncycastle.openpgp.PGPCompressedData; +import org.bouncycastle.openpgp.PGPEncryptedDataList; +import org.bouncycastle.openpgp.PGPLiteralData; +import org.bouncycastle.openpgp.PGPOnePassSignatureList; +import org.bouncycastle.openpgp.PGPSignatureList; + +public enum InputAlphabet { + /** + * A {@link PGPLiteralData} packet. + */ + LiteralData, + /** + * A {@link PGPSignatureList} object. + */ + Signatures, + /** + * A {@link PGPOnePassSignatureList} object. + */ + OnePassSignatures, + /** + * A {@link PGPCompressedData} packet. + * The contents of this packet MUST form a valid OpenPGP message, so a nested PDA is opened to verify + * its nested packet sequence. + */ + CompressedData, + /** + * A {@link PGPEncryptedDataList} object. + * This object combines multiple ESKs and the corresponding Symmetrically Encrypted + * (possibly Integrity Protected) Data packet. + */ + EncryptedData, + /** + * Marks the end of a (sub-) sequence. + * This input is given if the end of an OpenPGP message is reached. + * This might be the case for the end of the whole ciphertext, or the end of a packet with nested contents + * (e.g. the end of a Compressed Data packet). + */ + EndOfSequence +} diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/PushdownAutomaton.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/automaton/NestingPDA.java similarity index 78% rename from pgpainless-core/src/main/java/org/pgpainless/decryption_verification/PushdownAutomaton.java rename to pgpainless-core/src/main/java/org/pgpainless/decryption_verification/automaton/NestingPDA.java index 86075280..cf5ee674 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/PushdownAutomaton.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/automaton/NestingPDA.java @@ -1,17 +1,12 @@ -package org.pgpainless.decryption_verification; +package org.pgpainless.decryption_verification.automaton; -import org.bouncycastle.openpgp.PGPCompressedData; -import org.bouncycastle.openpgp.PGPEncryptedDataList; -import org.bouncycastle.openpgp.PGPLiteralData; -import org.bouncycastle.openpgp.PGPOnePassSignatureList; -import org.bouncycastle.openpgp.PGPSignatureList; import org.pgpainless.exception.MalformedOpenPgpMessageException; import java.util.Stack; -import static org.pgpainless.decryption_verification.PushdownAutomaton.StackAlphabet.msg; -import static org.pgpainless.decryption_verification.PushdownAutomaton.StackAlphabet.ops; -import static org.pgpainless.decryption_verification.PushdownAutomaton.StackAlphabet.terminus; +import static org.pgpainless.decryption_verification.automaton.StackAlphabet.msg; +import static org.pgpainless.decryption_verification.automaton.StackAlphabet.ops; +import static org.pgpainless.decryption_verification.automaton.StackAlphabet.terminus; /** * Pushdown Automaton to verify the correct syntax of OpenPGP messages during decryption. @@ -37,71 +32,18 @@ import static org.pgpainless.decryption_verification.PushdownAutomaton.StackAlph * * @see RFC4880 §11.3. OpenPGP Messages */ -public class PushdownAutomaton { - - public enum InputAlphabet { - /** - * A {@link PGPLiteralData} packet. - */ - LiteralData, - /** - * A {@link PGPSignatureList} object. - */ - Signatures, - /** - * A {@link PGPOnePassSignatureList} object. - */ - OnePassSignatures, - /** - * A {@link PGPCompressedData} packet. - * The contents of this packet MUST form a valid OpenPGP message, so a nested PDA is opened to verify - * its nested packet sequence. - */ - CompressedData, - /** - * A {@link PGPEncryptedDataList} object. - * This object combines multiple ESKs and the corresponding Symmetrically Encrypted - * (possibly Integrity Protected) Data packet. - */ - EncryptedData, - /** - * Marks the end of a (sub-) sequence. - * This input is given if the end of an OpenPGP message is reached. - * This might be the case for the end of the whole ciphertext, or the end of a packet with nested contents - * (e.g. the end of a Compressed Data packet). - */ - EndOfSequence - } - - public enum StackAlphabet { - /** - * OpenPGP Message. - */ - msg, - /** - * OnePassSignature (in case of BC this represents a OnePassSignatureList). - */ - ops, - /** - * ESK. Not used, as BC combines encrypted data with their encrypted session keys. - */ - esk, - /** - * Special symbol representing the end of the message. - */ - terminus - } +public class NestingPDA { /** * Set of states of the automaton. - * Each state defines its valid transitions in their {@link State#transition(InputAlphabet, PushdownAutomaton)} + * Each state defines its valid transitions in their {@link State#transition(InputAlphabet, NestingPDA)} * method. */ public enum State { OpenPgpMessage { @Override - State transition(InputAlphabet input, PushdownAutomaton automaton) throws MalformedOpenPgpMessageException { + State transition(InputAlphabet input, NestingPDA automaton) throws MalformedOpenPgpMessageException { StackAlphabet stackItem = automaton.popStack(); if (stackItem != msg) { throw new MalformedOpenPgpMessageException(this, input, stackItem); @@ -135,7 +77,7 @@ public class PushdownAutomaton { LiteralMessage { @Override - State transition(InputAlphabet input, PushdownAutomaton automaton) throws MalformedOpenPgpMessageException { + State transition(InputAlphabet input, NestingPDA automaton) throws MalformedOpenPgpMessageException { StackAlphabet stackItem = automaton.popStack(); switch (input) { @@ -165,7 +107,7 @@ public class PushdownAutomaton { CompressedMessage { @Override - State transition(InputAlphabet input, PushdownAutomaton automaton) throws MalformedOpenPgpMessageException { + State transition(InputAlphabet input, NestingPDA automaton) throws MalformedOpenPgpMessageException { StackAlphabet stackItem = automaton.popStack(); switch (input) { case Signatures: @@ -194,7 +136,7 @@ public class PushdownAutomaton { EncryptedMessage { @Override - State transition(InputAlphabet input, PushdownAutomaton automaton) throws MalformedOpenPgpMessageException { + State transition(InputAlphabet input, NestingPDA automaton) throws MalformedOpenPgpMessageException { StackAlphabet stackItem = automaton.popStack(); switch (input) { case Signatures: @@ -223,7 +165,7 @@ public class PushdownAutomaton { CorrespondingSignature { @Override - State transition(InputAlphabet input, PushdownAutomaton automaton) throws MalformedOpenPgpMessageException { + State transition(InputAlphabet input, NestingPDA automaton) throws MalformedOpenPgpMessageException { StackAlphabet stackItem = automaton.popStack(); if (stackItem == terminus && input == InputAlphabet.EndOfSequence && automaton.stack.isEmpty()) { return Valid; @@ -235,7 +177,7 @@ public class PushdownAutomaton { Valid { @Override - State transition(InputAlphabet input, PushdownAutomaton automaton) throws MalformedOpenPgpMessageException { + State transition(InputAlphabet input, NestingPDA automaton) throws MalformedOpenPgpMessageException { throw new MalformedOpenPgpMessageException(this, input, null); } }, @@ -252,15 +194,15 @@ public class PushdownAutomaton { * @return new state of the automaton * @throws MalformedOpenPgpMessageException in case of an illegal input symbol */ - abstract State transition(InputAlphabet input, PushdownAutomaton automaton) throws MalformedOpenPgpMessageException; + abstract State transition(InputAlphabet input, NestingPDA automaton) throws MalformedOpenPgpMessageException; } private final Stack stack = new Stack<>(); private State state; // Some OpenPGP packets have nested contents (e.g. compressed / encrypted data). - PushdownAutomaton nestedSequence = null; + NestingPDA nestedSequence = null; - public PushdownAutomaton() { + public NestingPDA() { state = State.OpenPgpMessage; stack.push(terminus); stack.push(msg); @@ -301,7 +243,7 @@ public class PushdownAutomaton { // If the processed packet contains nested sequence, open nested automaton for it if (input == InputAlphabet.CompressedData || input == InputAlphabet.EncryptedData) { - nestedSequence = new PushdownAutomaton(); + nestedSequence = new NestingPDA(); } } 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 new file mode 100644 index 00000000..6b989720 --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/automaton/PDA.java @@ -0,0 +1,237 @@ +package org.pgpainless.decryption_verification.automaton; + +import org.pgpainless.exception.MalformedOpenPgpMessageException; + +import java.util.Stack; + +import static org.pgpainless.decryption_verification.automaton.StackAlphabet.msg; +import static org.pgpainless.decryption_verification.automaton.StackAlphabet.ops; +import static org.pgpainless.decryption_verification.automaton.StackAlphabet.terminus; + +public class PDA { + /** + * Set of states of the automaton. + * Each state defines its valid transitions in their {@link NestingPDA.State#transition(InputAlphabet, NestingPDA)} + * method. + */ + public enum State { + + OpenPgpMessage { + @Override + State transition(InputAlphabet input, PDA automaton) throws MalformedOpenPgpMessageException { + StackAlphabet stackItem = automaton.popStack(); + if (stackItem != msg) { + throw new MalformedOpenPgpMessageException(this, input, stackItem); + } + switch (input) { + + case LiteralData: + return LiteralMessage; + + case Signatures: + automaton.pushStack(msg); + return OpenPgpMessage; + + case OnePassSignatures: + automaton.pushStack(ops); + automaton.pushStack(msg); + return OpenPgpMessage; + + case CompressedData: + return CompressedMessage; + + case EncryptedData: + return EncryptedMessage; + + case EndOfSequence: + default: + throw new MalformedOpenPgpMessageException(this, input, stackItem); + } + } + }, + + LiteralMessage { + @Override + State transition(InputAlphabet input, PDA automaton) throws MalformedOpenPgpMessageException { + StackAlphabet stackItem = automaton.popStack(); + switch (input) { + + case Signatures: + if (stackItem == ops) { + return CorrespondingSignature; + } else { + throw new MalformedOpenPgpMessageException(this, input, stackItem); + } + + case EndOfSequence: + if (stackItem == terminus && automaton.stack.isEmpty()) { + return Valid; + } else { + throw new MalformedOpenPgpMessageException(this, input, stackItem); + } + + case LiteralData: + case OnePassSignatures: + case CompressedData: + case EncryptedData: + default: + throw new MalformedOpenPgpMessageException(this, input, stackItem); + } + } + }, + + CompressedMessage { + @Override + State transition(InputAlphabet input, PDA automaton) throws MalformedOpenPgpMessageException { + StackAlphabet stackItem = automaton.popStack(); + switch (input) { + case Signatures: + if (stackItem == ops) { + return CorrespondingSignature; + } else { + throw new MalformedOpenPgpMessageException(this, input, stackItem); + } + + case EndOfSequence: + if (stackItem == terminus && automaton.stack.isEmpty()) { + return Valid; + } else { + throw new MalformedOpenPgpMessageException(this, input, stackItem); + } + + case LiteralData: + case OnePassSignatures: + case CompressedData: + case EncryptedData: + default: + throw new MalformedOpenPgpMessageException(this, input, stackItem); + } + } + }, + + EncryptedMessage { + @Override + State transition(InputAlphabet input, PDA automaton) throws MalformedOpenPgpMessageException { + StackAlphabet stackItem = automaton.popStack(); + switch (input) { + case Signatures: + if (stackItem == ops) { + return CorrespondingSignature; + } else { + throw new MalformedOpenPgpMessageException(this, input, stackItem); + } + + case EndOfSequence: + if (stackItem == terminus && automaton.stack.isEmpty()) { + return Valid; + } else { + throw new MalformedOpenPgpMessageException(this, input, stackItem); + } + + case LiteralData: + case OnePassSignatures: + case CompressedData: + case EncryptedData: + default: + throw new MalformedOpenPgpMessageException(this, input, stackItem); + } + } + }, + + CorrespondingSignature { + @Override + State transition(InputAlphabet input, PDA automaton) throws MalformedOpenPgpMessageException { + StackAlphabet stackItem = automaton.popStack(); + if (stackItem == terminus && input == InputAlphabet.EndOfSequence && automaton.stack.isEmpty()) { + return Valid; + } else { + throw new MalformedOpenPgpMessageException(this, input, stackItem); + } + } + }, + + Valid { + @Override + State transition(InputAlphabet input, PDA automaton) throws MalformedOpenPgpMessageException { + throw new MalformedOpenPgpMessageException(this, input, null); + } + }, + ; + + /** + * Pop the automatons stack and transition to another state. + * If no valid transition from the current state is available given the popped stack item and input symbol, + * a {@link MalformedOpenPgpMessageException} is thrown. + * Otherwise, the stack is manipulated according to the valid transition and the new state is returned. + * + * @param input input symbol + * @param automaton automaton + * @return new state of the automaton + * @throws MalformedOpenPgpMessageException in case of an illegal input symbol + */ + abstract State transition(InputAlphabet input, PDA automaton) throws MalformedOpenPgpMessageException; + } + + private final Stack stack = new Stack<>(); + private State state; + + public PDA() { + state = State.OpenPgpMessage; + stack.push(terminus); + stack.push(msg); + } + + 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("Transition from " + old + " to " + state + " via " + input + " with stack " + stackItem); + } + + /** + * Return the current state of the PDA. + * + * @return state + */ + public State getState() { + return state; + } + + /** + * Return true, if the PDA is in a valid state (the OpenPGP message is valid). + * + * @return true if valid, false otherwise + */ + public boolean isValid() { + return getState() == State.Valid && stack.isEmpty(); + } + + public void assertValid() throws MalformedOpenPgpMessageException { + if (!isValid()) { + throw new MalformedOpenPgpMessageException("Pushdown Automaton is not in an acceptable state: " + toString()); + } + } + + /** + * Pop an item from the stack. + * + * @return stack item + */ + private StackAlphabet popStack() { + return stack.pop(); + } + + /** + * Push an item onto the stack. + * + * @param item item + */ + private void pushStack(StackAlphabet item) { + stack.push(item); + } + + @Override + public String toString() { + return "State: " + state + " Stack: " + stack; + } +} diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/automaton/StackAlphabet.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/automaton/StackAlphabet.java new file mode 100644 index 00000000..97dad3d8 --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/automaton/StackAlphabet.java @@ -0,0 +1,20 @@ +package org.pgpainless.decryption_verification.automaton; + +public enum StackAlphabet { + /** + * OpenPGP Message. + */ + msg, + /** + * OnePassSignature (in case of BC this represents a OnePassSignatureList). + */ + ops, + /** + * ESK. Not used, as BC combines encrypted data with their encrypted session keys. + */ + esk, + /** + * Special symbol representing the end of the message. + */ + terminus +} diff --git a/pgpainless-core/src/main/java/org/pgpainless/exception/MalformedOpenPgpMessageException.java b/pgpainless-core/src/main/java/org/pgpainless/exception/MalformedOpenPgpMessageException.java index 9ce2284d..21b6e807 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/exception/MalformedOpenPgpMessageException.java +++ b/pgpainless-core/src/main/java/org/pgpainless/exception/MalformedOpenPgpMessageException.java @@ -4,8 +4,10 @@ package org.pgpainless.exception; -import org.bouncycastle.openpgp.PGPException; -import org.pgpainless.decryption_verification.PushdownAutomaton; +import org.pgpainless.decryption_verification.automaton.InputAlphabet; +import org.pgpainless.decryption_verification.automaton.NestingPDA; +import org.pgpainless.decryption_verification.automaton.PDA; +import org.pgpainless.decryption_verification.automaton.StackAlphabet; /** * Exception that gets thrown if the OpenPGP message is malformed. @@ -13,7 +15,7 @@ import org.pgpainless.decryption_verification.PushdownAutomaton; * * @see RFC4880 §11.3. OpenPGP Messages */ -public class MalformedOpenPgpMessageException extends PGPException { +public class MalformedOpenPgpMessageException extends RuntimeException { public MalformedOpenPgpMessageException(String message) { super(message); @@ -23,20 +25,17 @@ public class MalformedOpenPgpMessageException extends PGPException { super(message, cause); } - public MalformedOpenPgpMessageException(PushdownAutomaton.State state, - PushdownAutomaton.InputAlphabet input, - PushdownAutomaton.StackAlphabet stackItem) { + public MalformedOpenPgpMessageException(NestingPDA.State state, + InputAlphabet input, + StackAlphabet stackItem) { this("Invalid input: There is no legal transition from state '" + state + "' for input '" + input + "' when '" + stackItem + "' is on top of the stack."); } - public RTE toRuntimeException() { - return new RTE(this); + public MalformedOpenPgpMessageException(PDA.State state, InputAlphabet input, StackAlphabet stackItem) { + this("Invalid input: There is no legal transition from state '" + state + "' for input '" + input + "' when '" + stackItem + "' is on top of the stack."); } - public static class RTE extends RuntimeException { - - public RTE(MalformedOpenPgpMessageException e) { - super(e); - } + public MalformedOpenPgpMessageException(String message, PDA automaton) { + super(message + automaton.toString()); } } diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/info/KeyRingInfo.java b/pgpainless-core/src/main/java/org/pgpainless/key/info/KeyRingInfo.java index b818290b..fa4168dd 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/key/info/KeyRingInfo.java +++ b/pgpainless-core/src/main/java/org/pgpainless/key/info/KeyRingInfo.java @@ -1100,29 +1100,33 @@ public class KeyRingInfo { List signingKeys = getSigningSubkeys(); for (PGPPublicKey pk : signingKeys) { - PGPSecretKey sk = getSecretKey(pk.getKeyID()); - if (sk == null) { - // Missing secret key - continue; - } - S2K s2K = sk.getS2K(); - // Unencrypted key - if (s2K == null) { - return true; - } - - // Secret key on smart-card - int s2kType = s2K.getType(); - if (s2kType >= 100 && s2kType <= 110) { - continue; - } - // protected secret key - return true; + return isSecretKeyAvailable(pk.getKeyID()); } // No usable secret key found return false; } + public boolean isSecretKeyAvailable(long keyId) { + PGPSecretKey sk = getSecretKey(keyId); + if (sk == null) { + // Missing secret key + return false; + } + S2K s2K = sk.getS2K(); + // Unencrypted key + if (s2K == null) { + return true; + } + + // Secret key on smart-card + int s2kType = s2K.getType(); + if (s2kType >= 100 && s2kType <= 110) { + return false; + } + // protected secret key + return true; + } + private KeyAccessor getKeyAccessor(@Nullable String userId, long keyID) { if (getPublicKey(keyID) == null) { throw new NoSuchElementException("No subkey with key id " + Long.toHexString(keyID) + " found on this key."); 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 new file mode 100644 index 00000000..ae6390ce --- /dev/null +++ b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStreamTest.java @@ -0,0 +1,86 @@ +package org.pgpainless.decryption_verification; + +import org.bouncycastle.bcpg.ArmoredInputStream; +import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.util.io.Streams; +import org.junit.jupiter.api.Test; +import org.pgpainless.exception.MalformedOpenPgpMessageException; +import org.pgpainless.util.ArmoredInputStreamFactory; +import org.pgpainless.util.Passphrase; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.pgpainless.decryption_verification.PGPDecryptionStreamTest.COMP; +import static org.pgpainless.decryption_verification.PGPDecryptionStreamTest.COMP_COMP_LIT; +import static org.pgpainless.decryption_verification.PGPDecryptionStreamTest.COMP_LIT; +import static org.pgpainless.decryption_verification.PGPDecryptionStreamTest.LIT; +import static org.pgpainless.decryption_verification.PGPDecryptionStreamTest.LIT_LIT; +import static org.pgpainless.decryption_verification.PGPDecryptionStreamTest.PASSPHRASE; +import static org.pgpainless.decryption_verification.PGPDecryptionStreamTest.PLAINTEXT; +import static org.pgpainless.decryption_verification.PGPDecryptionStreamTest.SENC_LIT; +import static org.pgpainless.decryption_verification.PGPDecryptionStreamTest.SIG_LIT; + +public class OpenPgpMessageInputStreamTest { + + @Test + public void testProcessLIT() throws IOException, PGPException { + String plain = process(LIT, ConsumerOptions.get()); + assertEquals(PLAINTEXT, plain); + } + + @Test + public void testProcessLIT_LIT_fails() { + assertThrows(MalformedOpenPgpMessageException.class, + () -> process(LIT_LIT, ConsumerOptions.get())); + } + + @Test + public void testProcessCOMP_LIT() throws PGPException, IOException { + String plain = process(COMP_LIT, ConsumerOptions.get()); + assertEquals(PLAINTEXT, plain); + } + + @Test + public void testProcessCOMP_fails() { + assertThrows(MalformedOpenPgpMessageException.class, + () -> process(COMP, ConsumerOptions.get())); + } + + @Test + public void testProcessCOMP_COMP_LIT() throws PGPException, IOException { + String plain = process(COMP_COMP_LIT, ConsumerOptions.get()); + assertEquals(PLAINTEXT, plain); + } + + @Test + public void testProcessSIG_LIT() throws PGPException, IOException { + String plain = process(SIG_LIT, ConsumerOptions.get()); + assertEquals(PLAINTEXT, plain); + } + + @Test + public void testProcessSENC_LIT() throws PGPException, IOException { + String plain = process(SENC_LIT, ConsumerOptions.get().addDecryptionPassphrase(Passphrase.fromPassword(PASSPHRASE))); + assertEquals(PLAINTEXT, plain); + } + + private String process(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(); + } + + private 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); + return pgpIn; + } +} diff --git a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/PGPDecryptionStreamTest.java b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/PGPDecryptionStreamTest.java index bb2742b5..a84da9d5 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/PGPDecryptionStreamTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/PGPDecryptionStreamTest.java @@ -12,6 +12,8 @@ import org.bouncycastle.openpgp.PGPSignature; import org.bouncycastle.util.io.Streams; import org.junit.jupiter.api.Test; import org.pgpainless.PGPainless; +import org.pgpainless.algorithm.CompressionAlgorithm; +import org.pgpainless.encryption_signing.EncryptionOptions; import org.pgpainless.encryption_signing.EncryptionResult; import org.pgpainless.encryption_signing.EncryptionStream; import org.pgpainless.encryption_signing.ProducerOptions; @@ -19,6 +21,7 @@ import org.pgpainless.encryption_signing.SigningOptions; 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; @@ -33,7 +36,7 @@ import static org.junit.jupiter.api.Assertions.assertThrows; public class PGPDecryptionStreamTest { - private static final String KEY = "-----BEGIN PGP PRIVATE KEY BLOCK-----\n" + + public static final String KEY = "-----BEGIN PGP PRIVATE KEY BLOCK-----\n" + "Version: PGPainless\n" + "Comment: DA05 848F 37D4 68E6 F982 C889 7A70 1FC6 904D 3F4C\n" + "Comment: Alice \n" + @@ -58,9 +61,10 @@ public class PGPDecryptionStreamTest { "=THgv\n" + "-----END PGP PRIVATE KEY BLOCK-----"; - private static final String PLAINTEXT = "Hello, World!\n"; + public static final String PLAINTEXT = "Hello, World!\n"; + public static final String PASSPHRASE = "sw0rdf1sh"; - private static final String LIT = "" + + public static final String LIT = "" + "-----BEGIN PGP MESSAGE-----\n" + "Version: PGPainless\n" + "\n" + @@ -68,7 +72,7 @@ public class PGPDecryptionStreamTest { "=WGju\n" + "-----END PGP MESSAGE-----"; - private static final String LIT_LIT = "" + + public static final String LIT_LIT = "" + "-----BEGIN PGP MESSAGE-----\n" + "Version: BCPG v1.71\n" + "\n" + @@ -76,7 +80,7 @@ public class PGPDecryptionStreamTest { "=A91Q\n" + "-----END PGP MESSAGE-----"; - private static final String COMP_LIT = "" + + public static final String COMP_LIT = "" + "-----BEGIN PGP MESSAGE-----\n" + "Version: BCPG v1.71\n" + "\n" + @@ -84,7 +88,7 @@ public class PGPDecryptionStreamTest { "=ZYDg\n" + "-----END PGP MESSAGE-----"; - private static final String COMP = "" + + public static final String COMP = "" + "-----BEGIN PGP MESSAGE-----\n" + "Version: BCPG v1.71\n" + "\n" + @@ -92,7 +96,7 @@ public class PGPDecryptionStreamTest { "=MDzg\n" + "-----END PGP MESSAGE-----"; - private static final String COMP_COMP_LIT = "" + + public static final String COMP_COMP_LIT = "" + "-----BEGIN PGP MESSAGE-----\n" + "Version: BCPG v1.71\n" + "\n" + @@ -101,7 +105,7 @@ public class PGPDecryptionStreamTest { "=K9Zl\n" + "-----END PGP MESSAGE-----"; - private static final String SIG_LIT = "" + + public static final String SIG_LIT = "" + "-----BEGIN PGP MESSAGE-----\n" + "Version: BCPG v1.71\n" + "\n" + @@ -111,6 +115,15 @@ public class PGPDecryptionStreamTest { "=WKPn\n" + "-----END PGP MESSAGE-----"; + public static final String SENC_LIT = "" + + "-----BEGIN PGP MESSAGE-----\n" + + "Version: PGPainless\n" + + "\n" + + "jA0ECQMCuZ0qHNXWnGhg0j8Bdm1cxV65sYb7jDgb4rRMtdNpQ1dC4UpSYuk9YWS2\n" + + "DpNEijbX8b/P1UOK2kJczNDADMRegZuLEI+dNsBnJjk=\n" + + "=i4Y0\n" + + "-----END PGP MESSAGE-----"; + @Test public void genLIT() throws IOException { ArmoredOutputStream armorOut = new ArmoredOutputStream(System.out); @@ -125,7 +138,7 @@ public class PGPDecryptionStreamTest { public void processLIT() throws IOException, PGPException { ByteArrayInputStream bytesIn = new ByteArrayInputStream(LIT.getBytes(StandardCharsets.UTF_8)); ArmoredInputStream armorIn = ArmoredInputStreamFactory.get(bytesIn); - PGPDecryptionStream decIn = new PGPDecryptionStream(armorIn); + MessageDecryptionStream decIn = new MessageDecryptionStream(armorIn, ConsumerOptions.get()); ByteArrayOutputStream out = new ByteArrayOutputStream(); Streams.pipeAll(decIn, out); @@ -152,10 +165,10 @@ public class PGPDecryptionStreamTest { public void processLIT_LIT() throws IOException, PGPException { ByteArrayInputStream bytesIn = new ByteArrayInputStream(LIT_LIT.getBytes(StandardCharsets.UTF_8)); ArmoredInputStream armorIn = ArmoredInputStreamFactory.get(bytesIn); - PGPDecryptionStream decIn = new PGPDecryptionStream(armorIn); + MessageDecryptionStream decIn = new MessageDecryptionStream(armorIn, ConsumerOptions.get()); ByteArrayOutputStream out = new ByteArrayOutputStream(); - assertThrows(MalformedOpenPgpMessageException.RTE.class, () -> Streams.pipeAll(decIn, out)); + assertThrows(MalformedOpenPgpMessageException.class, () -> Streams.pipeAll(decIn, out)); } @Test @@ -175,7 +188,7 @@ public class PGPDecryptionStreamTest { public void processCOMP_LIT() throws IOException, PGPException { ByteArrayInputStream bytesIn = new ByteArrayInputStream(COMP_LIT.getBytes(StandardCharsets.UTF_8)); ArmoredInputStream armorIn = ArmoredInputStreamFactory.get(bytesIn); - PGPDecryptionStream decIn = new PGPDecryptionStream(armorIn); + MessageDecryptionStream decIn = new MessageDecryptionStream(armorIn, ConsumerOptions.get()); ByteArrayOutputStream out = new ByteArrayOutputStream(); Streams.pipeAll(decIn, out); @@ -198,8 +211,8 @@ public class PGPDecryptionStreamTest { public void processCOMP() throws IOException { ByteArrayInputStream bytesIn = new ByteArrayInputStream(COMP.getBytes(StandardCharsets.UTF_8)); ArmoredInputStream armorIn = ArmoredInputStreamFactory.get(bytesIn); - assertThrows(MalformedOpenPgpMessageException.RTE.class, () -> { - PGPDecryptionStream decIn = new PGPDecryptionStream(armorIn); + assertThrows(MalformedOpenPgpMessageException.class, () -> { + MessageDecryptionStream decIn = new MessageDecryptionStream(armorIn, ConsumerOptions.get()); Streams.drain(decIn); }); } @@ -228,7 +241,7 @@ public class PGPDecryptionStreamTest { public void processCOMP_COMP_LIT() throws PGPException, IOException { ByteArrayInputStream bytesIn = new ByteArrayInputStream(COMP_COMP_LIT.getBytes(StandardCharsets.UTF_8)); ArmoredInputStream armorIn = ArmoredInputStreamFactory.get(bytesIn); - PGPDecryptionStream decIn = new PGPDecryptionStream(armorIn); + MessageDecryptionStream decIn = new MessageDecryptionStream(armorIn, ConsumerOptions.get()); ByteArrayOutputStream out = new ByteArrayOutputStream(); Streams.pipeAll(decIn, out); @@ -279,7 +292,35 @@ public class PGPDecryptionStreamTest { public void processSIG_LIT() throws IOException, PGPException { ByteArrayInputStream bytesIn = new ByteArrayInputStream(SIG_LIT.getBytes(StandardCharsets.UTF_8)); ArmoredInputStream armorIn = ArmoredInputStreamFactory.get(bytesIn); - PGPDecryptionStream decIn = new PGPDecryptionStream(armorIn); + MessageDecryptionStream decIn = new MessageDecryptionStream(armorIn, ConsumerOptions.get()); + + ByteArrayOutputStream out = new ByteArrayOutputStream(); + Streams.pipeAll(decIn, out); + decIn.close(); + + System.out.println(out); + } + + @Test + public void genSENC_LIT() throws PGPException, IOException { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + EncryptionStream enc = PGPainless.encryptAndOrSign() + .onOutputStream(out) + .withOptions(ProducerOptions.encrypt(EncryptionOptions.get() + .addPassphrase(Passphrase.fromPassword(PASSPHRASE))) + .overrideCompressionAlgorithm(CompressionAlgorithm.UNCOMPRESSED)); + enc.write(PLAINTEXT.getBytes(StandardCharsets.UTF_8)); + enc.close(); + + System.out.println(out); + } + + @Test + public void processSENC_LIT() throws IOException, PGPException { + ByteArrayInputStream bytesIn = new ByteArrayInputStream(SENC_LIT.getBytes(StandardCharsets.UTF_8)); + ArmoredInputStream armorIn = ArmoredInputStreamFactory.get(bytesIn); + MessageDecryptionStream decIn = new MessageDecryptionStream(armorIn, ConsumerOptions.get() + .addDecryptionPassphrase(Passphrase.fromPassword(PASSPHRASE))); ByteArrayOutputStream out = new ByteArrayOutputStream(); Streams.pipeAll(decIn, out); diff --git a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/PushDownAutomatonTest.java b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/PushDownAutomatonTest.java deleted file mode 100644 index 1bd07308..00000000 --- a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/PushDownAutomatonTest.java +++ /dev/null @@ -1,205 +0,0 @@ -// SPDX-FileCopyrightText: 2022 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.decryption_verification; - -import org.junit.jupiter.api.Test; -import org.pgpainless.exception.MalformedOpenPgpMessageException; - -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.assertTrue; - -public class PushDownAutomatonTest { - - /** - * MSG is valid. - * - * @throws MalformedOpenPgpMessageException fail - */ - @Test - public void testSimpleLiteralMessageIsValid() throws MalformedOpenPgpMessageException { - PushdownAutomaton automaton = new PushdownAutomaton(); - automaton.next(PushdownAutomaton.InputAlphabet.LiteralData); - automaton.next(PushdownAutomaton.InputAlphabet.EndOfSequence); - - assertTrue(automaton.isValid()); - } - - /** - * OPS MSG SIG is valid. - * - * @throws MalformedOpenPgpMessageException fail - */ - @Test - public void testSimpleOpsSignedMesssageIsValid() throws MalformedOpenPgpMessageException { - PushdownAutomaton automaton = new PushdownAutomaton(); - automaton.next(PushdownAutomaton.InputAlphabet.OnePassSignatures); - automaton.next(PushdownAutomaton.InputAlphabet.LiteralData); - automaton.next(PushdownAutomaton.InputAlphabet.Signatures); - automaton.next(PushdownAutomaton.InputAlphabet.EndOfSequence); - - assertTrue(automaton.isValid()); - } - - /** - * SIG MSG is valid. - * - * @throws MalformedOpenPgpMessageException fail - */ - @Test - public void testSimplePrependSignedMessageIsValid() throws MalformedOpenPgpMessageException { - PushdownAutomaton automaton = new PushdownAutomaton(); - automaton.next(PushdownAutomaton.InputAlphabet.Signatures); - automaton.next(PushdownAutomaton.InputAlphabet.LiteralData); - automaton.next(PushdownAutomaton.InputAlphabet.EndOfSequence); - - assertTrue(automaton.isValid()); - } - - /** - * OPS COMP(MSG) SIG is valid. - * - * @throws MalformedOpenPgpMessageException fail - */ - @Test - public void testOPSSignedCompressedMessageIsValid() throws MalformedOpenPgpMessageException { - PushdownAutomaton automaton = new PushdownAutomaton(); - automaton.next(PushdownAutomaton.InputAlphabet.OnePassSignatures); - automaton.next(PushdownAutomaton.InputAlphabet.CompressedData); - automaton.next(PushdownAutomaton.InputAlphabet.LiteralData); - automaton.next(PushdownAutomaton.InputAlphabet.EndOfSequence); - automaton.next(PushdownAutomaton.InputAlphabet.Signatures); - automaton.next(PushdownAutomaton.InputAlphabet.EndOfSequence); - - assertTrue(automaton.isValid()); - } - - /** - * OPS ENC(COMP(COMP(MSG))) SIG is valid. - * - * @throws MalformedOpenPgpMessageException fail - */ - @Test - public void testOpsSignedEncryptedCompressedCompressedMessageIsValid() throws MalformedOpenPgpMessageException { - PushdownAutomaton automaton = new PushdownAutomaton(); - automaton.next(PushdownAutomaton.InputAlphabet.OnePassSignatures); - automaton.next(PushdownAutomaton.InputAlphabet.EncryptedData); - automaton.next(PushdownAutomaton.InputAlphabet.CompressedData); - automaton.next(PushdownAutomaton.InputAlphabet.CompressedData); - - automaton.next(PushdownAutomaton.InputAlphabet.LiteralData); - - automaton.next(PushdownAutomaton.InputAlphabet.EndOfSequence); - automaton.next(PushdownAutomaton.InputAlphabet.EndOfSequence); - automaton.next(PushdownAutomaton.InputAlphabet.EndOfSequence); - automaton.next(PushdownAutomaton.InputAlphabet.Signatures); - automaton.next(PushdownAutomaton.InputAlphabet.EndOfSequence); - - assertTrue(automaton.isValid()); - } - - /** - * MSG SIG is invalid. - * - * @throws MalformedOpenPgpMessageException fail - */ - @Test - public void testLiteralPlusSigsFails() throws MalformedOpenPgpMessageException { - PushdownAutomaton automaton = new PushdownAutomaton(); - automaton.next(PushdownAutomaton.InputAlphabet.LiteralData); - assertThrows(MalformedOpenPgpMessageException.class, - () -> automaton.next(PushdownAutomaton.InputAlphabet.Signatures)); - } - - /** - * MSG MSG is invalid. - * - * @throws MalformedOpenPgpMessageException fail - */ - @Test - public void testTwoLiteralDataPacketsFails() throws MalformedOpenPgpMessageException { - PushdownAutomaton automaton = new PushdownAutomaton(); - automaton.next(PushdownAutomaton.InputAlphabet.LiteralData); - assertThrows(MalformedOpenPgpMessageException.class, - () -> automaton.next(PushdownAutomaton.InputAlphabet.LiteralData)); - } - - /** - * OPS COMP(MSG MSG) SIG is invalid (two literal packets are illegal). - * - * @throws MalformedOpenPgpMessageException fail - */ - @Test - public void testOPSSignedMessageWithTwoLiteralDataPacketsFails() throws MalformedOpenPgpMessageException { - PushdownAutomaton automaton = new PushdownAutomaton(); - automaton.next(PushdownAutomaton.InputAlphabet.OnePassSignatures); - automaton.next(PushdownAutomaton.InputAlphabet.CompressedData); - automaton.next(PushdownAutomaton.InputAlphabet.LiteralData); - assertThrows(MalformedOpenPgpMessageException.class, - () -> automaton.next(PushdownAutomaton.InputAlphabet.LiteralData)); - } - - /** - * OPS COMP(MSG) MSG SIG is invalid. - * - * @throws MalformedOpenPgpMessageException fail - */ - @Test - public void testOPSSignedMessageWithTwoLiteralDataPacketsFails2() throws MalformedOpenPgpMessageException { - PushdownAutomaton automaton = new PushdownAutomaton(); - automaton.next(PushdownAutomaton.InputAlphabet.OnePassSignatures); - automaton.next(PushdownAutomaton.InputAlphabet.CompressedData); - automaton.next(PushdownAutomaton.InputAlphabet.LiteralData); - automaton.next(PushdownAutomaton.InputAlphabet.EndOfSequence); - assertThrows(MalformedOpenPgpMessageException.class, - () -> automaton.next(PushdownAutomaton.InputAlphabet.LiteralData)); - } - - /** - * OPS COMP(MSG SIG) is invalid (MSG SIG does not form valid nested message). - * - * @throws MalformedOpenPgpMessageException fail - */ - @Test - public void testCorrespondingSignaturesOfOpsSignedMessageAreLayerFurtherDownFails() throws MalformedOpenPgpMessageException { - PushdownAutomaton automaton = new PushdownAutomaton(); - automaton.next(PushdownAutomaton.InputAlphabet.OnePassSignatures); - automaton.next(PushdownAutomaton.InputAlphabet.CompressedData); - automaton.next(PushdownAutomaton.InputAlphabet.LiteralData); - assertThrows(MalformedOpenPgpMessageException.class, - () -> automaton.next(PushdownAutomaton.InputAlphabet.Signatures)); - } - - /** - * Empty COMP is invalid. - */ - @Test - public void testEmptyCompressedDataIsInvalid() throws MalformedOpenPgpMessageException { - PushdownAutomaton automaton = new PushdownAutomaton(); - automaton.next(PushdownAutomaton.InputAlphabet.CompressedData); - assertThrows(MalformedOpenPgpMessageException.class, - () -> automaton.next(PushdownAutomaton.InputAlphabet.EndOfSequence)); - } - - @Test - public void testOPSSignedEncryptedCompressedOPSSignedMessageIsValid() throws MalformedOpenPgpMessageException { - PushdownAutomaton automaton = new PushdownAutomaton(); - automaton.next(PushdownAutomaton.InputAlphabet.OnePassSignatures); - - automaton.next(PushdownAutomaton.InputAlphabet.EncryptedData); - automaton.next(PushdownAutomaton.InputAlphabet.OnePassSignatures); - - automaton.next(PushdownAutomaton.InputAlphabet.CompressedData); - automaton.next(PushdownAutomaton.InputAlphabet.LiteralData); - automaton.next(PushdownAutomaton.InputAlphabet.EndOfSequence); - - automaton.next(PushdownAutomaton.InputAlphabet.Signatures); - automaton.next(PushdownAutomaton.InputAlphabet.EndOfSequence); - - automaton.next(PushdownAutomaton.InputAlphabet.Signatures); - automaton.next(PushdownAutomaton.InputAlphabet.EndOfSequence); - - assertTrue(automaton.isValid()); - } -} diff --git a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/automaton/NestingPDATest.java b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/automaton/NestingPDATest.java new file mode 100644 index 00000000..8c1c4921 --- /dev/null +++ b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/automaton/NestingPDATest.java @@ -0,0 +1,205 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.decryption_verification.automaton; + +import org.junit.jupiter.api.Test; +import org.pgpainless.exception.MalformedOpenPgpMessageException; + +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class NestingPDATest { + + /** + * MSG is valid. + * + * @throws MalformedOpenPgpMessageException fail + */ + @Test + public void testSimpleLiteralMessageIsValid() throws MalformedOpenPgpMessageException { + NestingPDA automaton = new NestingPDA(); + automaton.next(InputAlphabet.LiteralData); + automaton.next(InputAlphabet.EndOfSequence); + + assertTrue(automaton.isValid()); + } + + /** + * OPS MSG SIG is valid. + * + * @throws MalformedOpenPgpMessageException fail + */ + @Test + public void testSimpleOpsSignedMesssageIsValid() throws MalformedOpenPgpMessageException { + NestingPDA automaton = new NestingPDA(); + automaton.next(InputAlphabet.OnePassSignatures); + automaton.next(InputAlphabet.LiteralData); + automaton.next(InputAlphabet.Signatures); + automaton.next(InputAlphabet.EndOfSequence); + + assertTrue(automaton.isValid()); + } + + /** + * SIG MSG is valid. + * + * @throws MalformedOpenPgpMessageException fail + */ + @Test + public void testSimplePrependSignedMessageIsValid() throws MalformedOpenPgpMessageException { + NestingPDA automaton = new NestingPDA(); + automaton.next(InputAlphabet.Signatures); + automaton.next(InputAlphabet.LiteralData); + automaton.next(InputAlphabet.EndOfSequence); + + assertTrue(automaton.isValid()); + } + + /** + * OPS COMP(MSG) SIG is valid. + * + * @throws MalformedOpenPgpMessageException fail + */ + @Test + public void testOPSSignedCompressedMessageIsValid() throws MalformedOpenPgpMessageException { + NestingPDA automaton = new NestingPDA(); + automaton.next(InputAlphabet.OnePassSignatures); + automaton.next(InputAlphabet.CompressedData); + automaton.next(InputAlphabet.LiteralData); + automaton.next(InputAlphabet.EndOfSequence); + automaton.next(InputAlphabet.Signatures); + automaton.next(InputAlphabet.EndOfSequence); + + assertTrue(automaton.isValid()); + } + + /** + * OPS ENC(COMP(COMP(MSG))) SIG is valid. + * + * @throws MalformedOpenPgpMessageException fail + */ + @Test + public void testOpsSignedEncryptedCompressedCompressedMessageIsValid() throws MalformedOpenPgpMessageException { + NestingPDA automaton = new NestingPDA(); + automaton.next(InputAlphabet.OnePassSignatures); + automaton.next(InputAlphabet.EncryptedData); + automaton.next(InputAlphabet.CompressedData); + automaton.next(InputAlphabet.CompressedData); + + automaton.next(InputAlphabet.LiteralData); + + automaton.next(InputAlphabet.EndOfSequence); + automaton.next(InputAlphabet.EndOfSequence); + automaton.next(InputAlphabet.EndOfSequence); + automaton.next(InputAlphabet.Signatures); + automaton.next(InputAlphabet.EndOfSequence); + + assertTrue(automaton.isValid()); + } + + /** + * MSG SIG is invalid. + * + * @throws MalformedOpenPgpMessageException fail + */ + @Test + public void testLiteralPlusSigsFails() throws MalformedOpenPgpMessageException { + NestingPDA automaton = new NestingPDA(); + automaton.next(InputAlphabet.LiteralData); + assertThrows(MalformedOpenPgpMessageException.class, + () -> automaton.next(InputAlphabet.Signatures)); + } + + /** + * MSG MSG is invalid. + * + * @throws MalformedOpenPgpMessageException fail + */ + @Test + public void testTwoLiteralDataPacketsFails() throws MalformedOpenPgpMessageException { + NestingPDA automaton = new NestingPDA(); + automaton.next(InputAlphabet.LiteralData); + assertThrows(MalformedOpenPgpMessageException.class, + () -> automaton.next(InputAlphabet.LiteralData)); + } + + /** + * OPS COMP(MSG MSG) SIG is invalid (two literal packets are illegal). + * + * @throws MalformedOpenPgpMessageException fail + */ + @Test + public void testOPSSignedMessageWithTwoLiteralDataPacketsFails() throws MalformedOpenPgpMessageException { + NestingPDA automaton = new NestingPDA(); + automaton.next(InputAlphabet.OnePassSignatures); + automaton.next(InputAlphabet.CompressedData); + automaton.next(InputAlphabet.LiteralData); + assertThrows(MalformedOpenPgpMessageException.class, + () -> automaton.next(InputAlphabet.LiteralData)); + } + + /** + * OPS COMP(MSG) MSG SIG is invalid. + * + * @throws MalformedOpenPgpMessageException fail + */ + @Test + public void testOPSSignedMessageWithTwoLiteralDataPacketsFails2() throws MalformedOpenPgpMessageException { + NestingPDA automaton = new NestingPDA(); + automaton.next(InputAlphabet.OnePassSignatures); + automaton.next(InputAlphabet.CompressedData); + automaton.next(InputAlphabet.LiteralData); + automaton.next(InputAlphabet.EndOfSequence); + assertThrows(MalformedOpenPgpMessageException.class, + () -> automaton.next(InputAlphabet.LiteralData)); + } + + /** + * OPS COMP(MSG SIG) is invalid (MSG SIG does not form valid nested message). + * + * @throws MalformedOpenPgpMessageException fail + */ + @Test + public void testCorrespondingSignaturesOfOpsSignedMessageAreLayerFurtherDownFails() throws MalformedOpenPgpMessageException { + NestingPDA automaton = new NestingPDA(); + automaton.next(InputAlphabet.OnePassSignatures); + automaton.next(InputAlphabet.CompressedData); + automaton.next(InputAlphabet.LiteralData); + assertThrows(MalformedOpenPgpMessageException.class, + () -> automaton.next(InputAlphabet.Signatures)); + } + + /** + * Empty COMP is invalid. + */ + @Test + public void testEmptyCompressedDataIsInvalid() throws MalformedOpenPgpMessageException { + NestingPDA automaton = new NestingPDA(); + automaton.next(InputAlphabet.CompressedData); + assertThrows(MalformedOpenPgpMessageException.class, + () -> automaton.next(InputAlphabet.EndOfSequence)); + } + + @Test + public void testOPSSignedEncryptedCompressedOPSSignedMessageIsValid() throws MalformedOpenPgpMessageException { + NestingPDA automaton = new NestingPDA(); + automaton.next(InputAlphabet.OnePassSignatures); + + automaton.next(InputAlphabet.EncryptedData); + automaton.next(InputAlphabet.OnePassSignatures); + + automaton.next(InputAlphabet.CompressedData); + automaton.next(InputAlphabet.LiteralData); + automaton.next(InputAlphabet.EndOfSequence); + + automaton.next(InputAlphabet.Signatures); + automaton.next(InputAlphabet.EndOfSequence); + + automaton.next(InputAlphabet.Signatures); + automaton.next(InputAlphabet.EndOfSequence); + + assertTrue(automaton.isValid()); + } +} diff --git a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/automaton/PDATest.java b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/automaton/PDATest.java new file mode 100644 index 00000000..6e8a38d6 --- /dev/null +++ b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/automaton/PDATest.java @@ -0,0 +1,75 @@ +package org.pgpainless.decryption_verification.automaton; + +import org.junit.jupiter.api.Test; +import org.pgpainless.exception.MalformedOpenPgpMessageException; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class PDATest { + + + /** + * MSG is valid. + * + * @throws MalformedOpenPgpMessageException fail + */ + @Test + public void testSimpleLiteralMessageIsValid() throws MalformedOpenPgpMessageException { + PDA automaton = new PDA(); + automaton.next(InputAlphabet.LiteralData); + automaton.next(InputAlphabet.EndOfSequence); + + assertTrue(automaton.isValid()); + } + + /** + * OPS MSG SIG is valid. + * + * @throws MalformedOpenPgpMessageException fail + */ + @Test + public void testSimpleOpsSignedMesssageIsValid() throws MalformedOpenPgpMessageException { + PDA automaton = new PDA(); + automaton.next(InputAlphabet.OnePassSignatures); + automaton.next(InputAlphabet.LiteralData); + automaton.next(InputAlphabet.Signatures); + automaton.next(InputAlphabet.EndOfSequence); + + assertTrue(automaton.isValid()); + } + + + /** + * SIG MSG is valid. + * + * @throws MalformedOpenPgpMessageException fail + */ + @Test + public void testSimplePrependSignedMessageIsValid() throws MalformedOpenPgpMessageException { + PDA automaton = new PDA(); + automaton.next(InputAlphabet.Signatures); + automaton.next(InputAlphabet.LiteralData); + automaton.next(InputAlphabet.EndOfSequence); + + assertTrue(automaton.isValid()); + } + + + /** + * OPS COMP(MSG) SIG is valid. + * + * @throws MalformedOpenPgpMessageException fail + */ + @Test + public void testOPSSignedCompressedMessageIsValid() throws MalformedOpenPgpMessageException { + PDA automaton = new PDA(); + automaton.next(InputAlphabet.OnePassSignatures); + automaton.next(InputAlphabet.CompressedData); + // Here would be a nested PDA for the LiteralData packet + automaton.next(InputAlphabet.Signatures); + automaton.next(InputAlphabet.EndOfSequence); + + assertTrue(automaton.isValid()); + } + +} From 8d6db322a1d21259e85e3004ba0bb4a04e5f31e0 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 13 Sep 2022 20:22:31 +0200 Subject: [PATCH 04/80] Fix tests --- .../OpenPgpMessageInputStream.java | 57 ++++++++++++++++--- .../automaton/PDA.java | 9 ++- .../OpenPgpMessageInputStreamTest.java | 12 ++++ .../PGPDecryptionStreamTest.java | 30 ++++++++++ 4 files changed, 98 insertions(+), 10 deletions(-) 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 549fe46b..788be4ab 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 @@ -1,6 +1,5 @@ package org.pgpainless.decryption_verification; -import com.sun.tools.javac.code.Attribute; import org.bouncycastle.bcpg.BCPGInputStream; import org.bouncycastle.bcpg.BCPGOutputStream; import org.bouncycastle.bcpg.OnePassSignaturePacket; @@ -25,13 +24,14 @@ import org.bouncycastle.openpgp.PGPSignature; import org.bouncycastle.openpgp.PGPSignatureList; import org.bouncycastle.openpgp.operator.PBEDataDecryptorFactory; import org.bouncycastle.openpgp.operator.PublicKeyDataDecryptorFactory; +import org.bouncycastle.openpgp.operator.SessionKeyDataDecryptorFactory; import org.pgpainless.PGPainless; import org.pgpainless.algorithm.EncryptionPurpose; import org.pgpainless.algorithm.OpenPgpPacket; import org.pgpainless.decryption_verification.automaton.InputAlphabet; import org.pgpainless.decryption_verification.automaton.PDA; -import org.pgpainless.exception.MalformedOpenPgpMessageException; import org.pgpainless.exception.MessageNotIntegrityProtectedException; +import org.pgpainless.exception.MissingDecryptionMethodException; import org.pgpainless.implementation.ImplementationFactory; import org.pgpainless.key.info.KeyRingInfo; import org.pgpainless.key.protection.SecretKeyRingProtector; @@ -114,7 +114,27 @@ public class OpenPgpMessageInputStream extends InputStream { SortedESKs esks = new SortedESKs(encDataList); - // TODO: try session keys + if (options.getSessionKey() != null) { + SessionKeyDataDecryptorFactory decryptorFactory = ImplementationFactory.getInstance() + .getSessionKeyDataDecryptorFactory(options.getSessionKey()); + // TODO: Replace with encDataList.addSessionKeyDecryptionMethod(sessionKey) + PGPEncryptedData esk = esks.all().get(0); + try { + if (esk instanceof PGPPBEEncryptedData) { + PGPPBEEncryptedData skesk = (PGPPBEEncryptedData) esk; + in = skesk.getDataStream(decryptorFactory); + break loop; + } else if (esk instanceof PGPPublicKeyEncryptedData) { + PGPPublicKeyEncryptedData pkesk = (PGPPublicKeyEncryptedData) esk; + in = pkesk.getDataStream(decryptorFactory); + break loop; + } else { + throw new RuntimeException("Unknown ESK class type: " + esk.getClass().getName()); + } + } catch (PGPException e) { + // Session key mismatch? + } + } // Try passwords for (PGPPBEEncryptedData skesk : esks.skesks) { @@ -174,7 +194,7 @@ public class OpenPgpMessageInputStream extends InputStream { // TODO: try interactive password callbacks - break loop; + throw new MissingDecryptionMethodException("No working decryption method found."); case MARKER: bcpgIn.readPacket(); // skip marker packet @@ -256,6 +276,7 @@ public class OpenPgpMessageInputStream extends InputStream { if (tag == PacketTags.SIGNATURE) { SignaturePacket sigPacket = (SignaturePacket) packet; sigPacket.encode(bcpgOut); + tag = bcpgIn.nextPacketTag(); } } bcpgOut.close(); @@ -270,16 +291,21 @@ public class OpenPgpMessageInputStream extends InputStream { @Override public int read() throws IOException { int r = -1; - try { - r = in.read(); - } catch (IOException e) { - // + if (in != null) { + try { + r = in.read(); + } catch (IOException e) { + // + } } if (r == -1) { if (in instanceof OpenPgpMessageInputStream) { + System.out.println("Read -1: close " + automaton); in.close(); + in = null; } else { try { + System.out.println("Walk " + automaton); walk(); } catch (PGPException e) { throw new RuntimeException(e); @@ -291,8 +317,15 @@ public class OpenPgpMessageInputStream extends InputStream { @Override public void close() throws IOException { + if (in == null) { + System.out.println("Close " + automaton); + automaton.next(InputAlphabet.EndOfSequence); + automaton.assertValid(); + return; + } try { in.close(); + in = null; // Nested streams (except LiteralData) need to be closed. if (automaton.getState() != PDA.State.LiteralMessage) { automaton.next(InputAlphabet.EndOfSequence); @@ -327,5 +360,13 @@ public class OpenPgpMessageInputStream extends InputStream { } } } + + public List all() { + List esks = new ArrayList<>(); + esks.addAll(skesks); + esks.addAll(pkesks); + esks.addAll(anonPkesks); + return esks; + } } } 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 6b989720..a3384070 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 @@ -9,6 +9,9 @@ import static org.pgpainless.decryption_verification.automaton.StackAlphabet.ops import static org.pgpainless.decryption_verification.automaton.StackAlphabet.terminus; public class PDA { + + private static int ID = 0; + /** * Set of states of the automaton. * Each state defines its valid transitions in their {@link NestingPDA.State#transition(InputAlphabet, NestingPDA)} @@ -174,18 +177,20 @@ public class PDA { private final Stack stack = new Stack<>(); private State state; + private int id; public PDA() { state = State.OpenPgpMessage; stack.push(terminus); stack.push(msg); + this.id = ID++; } 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("Transition from " + old + " to " + state + " via " + input + " with stack " + stackItem); + System.out.println(id + ": Transition from " + old + " to " + state + " via " + input + " with stack " + stackItem); } /** @@ -232,6 +237,6 @@ public class PDA { @Override public String toString() { - return "State: " + state + " Stack: " + stack; + return "PDA " + id + ": State: " + state + " Stack: " + stack; } } 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 ae6390ce..5b42101c 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 @@ -2,8 +2,10 @@ package org.pgpainless.decryption_verification; import org.bouncycastle.bcpg.ArmoredInputStream; import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.PGPSecretKeyRing; import org.bouncycastle.util.io.Streams; import org.junit.jupiter.api.Test; +import org.pgpainless.PGPainless; import org.pgpainless.exception.MalformedOpenPgpMessageException; import org.pgpainless.util.ArmoredInputStreamFactory; import org.pgpainless.util.Passphrase; @@ -18,9 +20,11 @@ import static org.junit.jupiter.api.Assertions.assertThrows; import static org.pgpainless.decryption_verification.PGPDecryptionStreamTest.COMP; import static org.pgpainless.decryption_verification.PGPDecryptionStreamTest.COMP_COMP_LIT; import static org.pgpainless.decryption_verification.PGPDecryptionStreamTest.COMP_LIT; +import static org.pgpainless.decryption_verification.PGPDecryptionStreamTest.KEY; import static org.pgpainless.decryption_verification.PGPDecryptionStreamTest.LIT; import static org.pgpainless.decryption_verification.PGPDecryptionStreamTest.LIT_LIT; import static org.pgpainless.decryption_verification.PGPDecryptionStreamTest.PASSPHRASE; +import static org.pgpainless.decryption_verification.PGPDecryptionStreamTest.PENC_COMP_LIT; import static org.pgpainless.decryption_verification.PGPDecryptionStreamTest.PLAINTEXT; import static org.pgpainless.decryption_verification.PGPDecryptionStreamTest.SENC_LIT; import static org.pgpainless.decryption_verification.PGPDecryptionStreamTest.SIG_LIT; @@ -69,6 +73,14 @@ public class OpenPgpMessageInputStreamTest { assertEquals(PLAINTEXT, plain); } + @Test + public void testProcessPENC_COMP_LIT() throws IOException, PGPException { + PGPSecretKeyRing secretKeys = PGPainless.readKeyRing().secretKeyRing(KEY); + String plain = process(PENC_COMP_LIT, ConsumerOptions.get() + .addDecryptionKey(secretKeys)); + assertEquals(PLAINTEXT, plain); + } + private String process(String armoredMessage, ConsumerOptions options) throws PGPException, IOException { OpenPgpMessageInputStream in = get(armoredMessage, options); ByteArrayOutputStream out = new ByteArrayOutputStream(); diff --git a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/PGPDecryptionStreamTest.java b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/PGPDecryptionStreamTest.java index a84da9d5..8da44ec8 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/PGPDecryptionStreamTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/PGPDecryptionStreamTest.java @@ -7,6 +7,7 @@ import org.bouncycastle.openpgp.PGPCompressedDataGenerator; import org.bouncycastle.openpgp.PGPException; import org.bouncycastle.openpgp.PGPLiteralData; import org.bouncycastle.openpgp.PGPLiteralDataGenerator; +import org.bouncycastle.openpgp.PGPPublicKeyRing; import org.bouncycastle.openpgp.PGPSecretKeyRing; import org.bouncycastle.openpgp.PGPSignature; import org.bouncycastle.util.io.Streams; @@ -124,6 +125,17 @@ public class PGPDecryptionStreamTest { "=i4Y0\n" + "-----END PGP MESSAGE-----"; + public static final String PENC_COMP_LIT = "" + + "-----BEGIN PGP MESSAGE-----\n" + + "Version: PGPainless\n" + + "\n" + + "hF4Dyqa/GWUy6WsSAQdAQ62BwmUt8Iby0+jvrLhMgST79KR/as+dyl0nf1uki2sw\n" + + "Thg1Ojtf0hOyJgcpQ4nP2Q0wYFR0F1sCydaIlTGreYZHlGtybP7/Ml6KNZILTRWP\n" + + "0kYBkGBgK7oQWRIVyoF2POvEP6EX1X8nvQk7O3NysVdRVbnia7gE3AzRYuha4kxs\n" + + "pI6xJkntLMS3K6him1Y9FHINIASFSB+C\n" + + "=5p00\n" + + "-----END PGP MESSAGE-----"; + @Test public void genLIT() throws IOException { ArmoredOutputStream armorOut = new ArmoredOutputStream(System.out); @@ -328,4 +340,22 @@ public class PGPDecryptionStreamTest { System.out.println(out); } + + @Test + public void genPENC_COMP_LIT() throws IOException, PGPException { + PGPSecretKeyRing secretKeys = PGPainless.readKeyRing().secretKeyRing(KEY); + PGPPublicKeyRing cert = PGPainless.extractCertificate(secretKeys); + + ByteArrayOutputStream out = new ByteArrayOutputStream(); + EncryptionStream enc = PGPainless.encryptAndOrSign() + .onOutputStream(out) + .withOptions(ProducerOptions.encrypt(EncryptionOptions.get() + .addRecipient(cert)) + .overrideCompressionAlgorithm(CompressionAlgorithm.ZLIB)); + + Streams.pipeAll(new ByteArrayInputStream(PLAINTEXT.getBytes(StandardCharsets.UTF_8)), enc); + enc.close(); + + System.out.println(out); + } } From 80e98a02ac26eb35edcd46ff5b0059612e3d8708 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 14 Sep 2022 19:29:47 +0200 Subject: [PATCH 05/80] Work on getting signature verification to function again --- .../OpenPgpMessageInputStream.java | 145 +++++++++++++----- .../automaton/PDA.java | 7 + 2 files changed, 116 insertions(+), 36 deletions(-) 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 788be4ab..e95c9ff7 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 @@ -30,19 +30,24 @@ import org.pgpainless.algorithm.EncryptionPurpose; import org.pgpainless.algorithm.OpenPgpPacket; import org.pgpainless.decryption_verification.automaton.InputAlphabet; import org.pgpainless.decryption_verification.automaton.PDA; +import org.pgpainless.decryption_verification.automaton.StackAlphabet; +import org.pgpainless.exception.MalformedOpenPgpMessageException; import org.pgpainless.exception.MessageNotIntegrityProtectedException; import org.pgpainless.exception.MissingDecryptionMethodException; import org.pgpainless.implementation.ImplementationFactory; import org.pgpainless.key.info.KeyRingInfo; import org.pgpainless.key.protection.SecretKeyRingProtector; import org.pgpainless.key.protection.UnlockSecretKey; +import org.pgpainless.signature.consumer.DetachedSignatureCheck; import org.pgpainless.util.Passphrase; import org.pgpainless.util.Tuple; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; +import java.io.UnsupportedEncodingException; import java.util.ArrayList; +import java.util.Collection; import java.util.List; public class OpenPgpMessageInputStream extends InputStream { @@ -52,12 +57,12 @@ public class OpenPgpMessageInputStream extends InputStream { protected final BCPGInputStream bcpgIn; protected InputStream in; - private List signatures = new ArrayList<>(); - private List onePassSignatures = new ArrayList<>(); + private boolean closed = false; + + private Signatures signatures = new Signatures(); public OpenPgpMessageInputStream(InputStream inputStream, ConsumerOptions options) throws IOException, PGPException { - this.options = options; // TODO: Use BCPGInputStream.wrap(inputStream); if (inputStream instanceof BCPGInputStream) { this.bcpgIn = (BCPGInputStream) inputStream; @@ -65,41 +70,61 @@ public class OpenPgpMessageInputStream extends InputStream { this.bcpgIn = new BCPGInputStream(inputStream); } - walk(); + this.options = options; + this.signatures.addDetachedSignatures(options.getDetachedSignatures()); + + consumePackets(); } - private void walk() throws IOException, PGPException { - loop: while (true) { - - int tag = bcpgIn.nextPacketTag(); - if (tag == -1) { - break loop; - } - + /** + * This method consumes OpenPGP packets from the current {@link BCPGInputStream}. + * Once it reaches a "nested" OpenPGP packet (Literal Data, Compressed Data, Encrypted Data), it sets

in
+ * to the nested stream and breaks the loop. + * The nested stream is either a simple {@link InputStream} (in case of Literal Data), or another + * {@link OpenPgpMessageInputStream} in case of Compressed and Encrypted Data. + * + * @throws IOException + * @throws PGPException + */ + private void consumePackets() + throws IOException, PGPException { + int tag; + loop: while ((tag = bcpgIn.nextPacketTag()) != -1) { OpenPgpPacket nextPacket = OpenPgpPacket.requireFromTag(tag); 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(); break loop; + // Compressed Data - the content contains another OpenPGP message case COMP: automaton.next(InputAlphabet.CompressedData); PGPCompressedData compressedData = new PGPCompressedData(bcpgIn); in = new OpenPgpMessageInputStream(compressedData.getDataStream(), options); break loop; + // One Pass Signatures case OPS: automaton.next(InputAlphabet.OnePassSignatures); - readOnePassSignatures(); + signatures.addOnePassSignatures(readOnePassSignatures()); break; + // Signatures - either prepended to the message, or corresponding to the One Pass Signatures case SIG: automaton.next(InputAlphabet.Signatures); - readSignatures(); + PGPSignatureList signatureList = readSignatures(); + if (automaton.peekStack() == StackAlphabet.ops) { + signatures.addOnePassCorrespondingSignatures(signatureList); + } else { + signatures.addPrependedSignatures(signatureList); + } break; + // Encrypted Data (ESKs and SED/SEIPD are parsed the same by BC) case PKESK: case SKESK: case SED: @@ -200,6 +225,7 @@ public class OpenPgpMessageInputStream extends InputStream { bcpgIn.readPacket(); // skip marker packet break; + // Key Packets are illegal in this context case SK: case PK: case SSK: @@ -209,13 +235,14 @@ public class OpenPgpMessageInputStream extends InputStream { case UATTR: case MOD: - break; + throw new MalformedOpenPgpMessageException("Unexpected Packet in Stream: " + nextPacket); + // Experimental Packets are not supported case EXP_1: case EXP_2: case EXP_3: case EXP_4: - break; + throw new MalformedOpenPgpMessageException("Unsupported Packet in Stream: " + nextPacket); } } } @@ -247,7 +274,7 @@ public class OpenPgpMessageInputStream extends InputStream { return null; } - private void readOnePassSignatures() throws IOException { + private PGPOnePassSignatureList readOnePassSignatures() throws IOException { ByteArrayOutputStream buf = new ByteArrayOutputStream(); BCPGOutputStream bcpgOut = new BCPGOutputStream(buf); int tag = bcpgIn.nextPacketTag(); @@ -262,12 +289,10 @@ public class OpenPgpMessageInputStream extends InputStream { PGPObjectFactory objectFactory = ImplementationFactory.getInstance().getPGPObjectFactory(buf.toByteArray()); PGPOnePassSignatureList signatureList = (PGPOnePassSignatureList) objectFactory.nextObject(); - for (PGPOnePassSignature ops : signatureList) { - onePassSignatures.add(ops); - } + return signatureList; } - private void readSignatures() throws IOException { + private PGPSignatureList readSignatures() throws IOException { ByteArrayOutputStream buf = new ByteArrayOutputStream(); BCPGOutputStream bcpgOut = new BCPGOutputStream(buf); int tag = bcpgIn.nextPacketTag(); @@ -283,9 +308,7 @@ public class OpenPgpMessageInputStream extends InputStream { PGPObjectFactory objectFactory = ImplementationFactory.getInstance().getPGPObjectFactory(buf.toByteArray()); PGPSignatureList signatureList = (PGPSignatureList) objectFactory.nextObject(); - for (PGPSignature signature : signatureList) { - signatures.add(signature); - } + return signatureList; } @Override @@ -298,15 +321,17 @@ public class OpenPgpMessageInputStream extends InputStream { // } } - if (r == -1) { + if (r != -1) { + byte b = (byte) r; + signatures.update(b); + } else { if (in instanceof OpenPgpMessageInputStream) { - System.out.println("Read -1: close " + automaton); in.close(); in = null; } else { try { System.out.println("Walk " + automaton); - walk(); + consumePackets(); } catch (PGPException e) { throw new RuntimeException(e); } @@ -317,25 +342,24 @@ public class OpenPgpMessageInputStream extends InputStream { @Override public void close() throws IOException { - if (in == null) { - System.out.println("Close " + automaton); - automaton.next(InputAlphabet.EndOfSequence); - automaton.assertValid(); + if (closed) { return; } - try { + + if (in != null) { in.close(); - in = null; - // Nested streams (except LiteralData) need to be closed. + in = null; // TODO: Collect result of in before nulling if (automaton.getState() != PDA.State.LiteralMessage) { automaton.next(InputAlphabet.EndOfSequence); automaton.assertValid(); } - } catch (IOException e) { - // + } else { + automaton.next(InputAlphabet.EndOfSequence); + automaton.assertValid(); } super.close(); + closed = true; } private static class SortedESKs { @@ -369,4 +393,53 @@ public class OpenPgpMessageInputStream extends InputStream { return esks; } } + + private static class Signatures { + List detachedSignatures = new ArrayList<>(); + List prependedSignatures = new ArrayList<>(); + List onePassSignatures = new ArrayList<>(); + List correspondingSignatures = new ArrayList<>(); + + void addDetachedSignatures(Collection signatures) { + this.detachedSignatures.addAll(signatures); + } + + void addPrependedSignatures(PGPSignatureList signatures) { + for (PGPSignature signature : signatures) { + this.prependedSignatures.add(signature); + } + } + + void addOnePassSignatures(PGPOnePassSignatureList signatures) { + for (PGPOnePassSignature ops : signatures) { + this.onePassSignatures.add(ops); + } + } + + void addOnePassCorrespondingSignatures(PGPSignatureList signatures) { + for (PGPSignature signature : signatures) { + correspondingSignatures.add(signature); + } + } + + public void update(byte b) { + /** + for (PGPSignature prepended : prependedSignatures) { + prepended.update(b); + } + for (PGPOnePassSignature ops : onePassSignatures) { + ops.update(b); + } + for (PGPSignature detached : detachedSignatures) { + detached.update(b); + } + */ + } + + public void finish() { + for (PGPSignature detached : detachedSignatures) { + + } + } + } } 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 a3384070..793a3451 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 @@ -202,6 +202,13 @@ public class PDA { return state; } + public StackAlphabet peekStack() { + if (stack.isEmpty()) { + return null; + } + return stack.peek(); + } + /** * Return true, if the PDA is in a valid state (the OpenPGP message is valid). * From 8625b2086c0fd4b0bec483a965875264d5993877 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 14 Sep 2022 19:41:22 +0200 Subject: [PATCH 06/80] Clean close() method --- .../OpenPgpMessageInputStream.java | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) 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 e95c9ff7..d95e3e3c 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 @@ -348,17 +348,11 @@ public class OpenPgpMessageInputStream extends InputStream { if (in != null) { in.close(); - in = null; // TODO: Collect result of in before nulling - if (automaton.getState() != PDA.State.LiteralMessage) { - automaton.next(InputAlphabet.EndOfSequence); - automaton.assertValid(); - } - } else { - automaton.next(InputAlphabet.EndOfSequence); - automaton.assertValid(); + in = null; } - super.close(); + automaton.next(InputAlphabet.EndOfSequence); + automaton.assertValid(); closed = true; } From 3ac17281ea4531bb98e611c6fe62f375c8b99863 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 14 Sep 2022 20:10:42 +0200 Subject: [PATCH 07/80] Add read(b,off,len) --- .../OpenPgpMessageInputStream.java | 71 +++++++++++++------ 1 file changed, 50 insertions(+), 21 deletions(-) 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 d95e3e3c..4c6a85c8 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 @@ -38,14 +38,12 @@ import org.pgpainless.implementation.ImplementationFactory; import org.pgpainless.key.info.KeyRingInfo; import org.pgpainless.key.protection.SecretKeyRingProtector; import org.pgpainless.key.protection.UnlockSecretKey; -import org.pgpainless.signature.consumer.DetachedSignatureCheck; import org.pgpainless.util.Passphrase; import org.pgpainless.util.Tuple; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; -import java.io.UnsupportedEncodingException; import java.util.ArrayList; import java.util.Collection; import java.util.List; @@ -88,6 +86,7 @@ public class OpenPgpMessageInputStream extends InputStream { */ private void consumePackets() throws IOException, PGPException { + System.out.println("Walk " + automaton); int tag; loop: while ((tag = bcpgIn.nextPacketTag()) != -1) { OpenPgpPacket nextPacket = OpenPgpPacket.requireFromTag(tag); @@ -237,7 +236,7 @@ public class OpenPgpMessageInputStream extends InputStream { case MOD: throw new MalformedOpenPgpMessageException("Unexpected Packet in Stream: " + nextPacket); - // Experimental Packets are not supported + // Experimental Packets are not supported case EXP_1: case EXP_2: case EXP_3: @@ -313,15 +312,19 @@ public class OpenPgpMessageInputStream extends InputStream { @Override public int read() throws IOException { - int r = -1; - if (in != null) { - try { - r = in.read(); - } catch (IOException e) { - // - } + if (in == null) { + automaton.assertValid(); + return -1; } - if (r != -1) { + + int r; + try { + r = in.read(); + } catch (IOException e) { + r = -1; + } + boolean eos = r == -1; + if (!eos) { byte b = (byte) r; signatures.update(b); } else { @@ -330,7 +333,32 @@ public class OpenPgpMessageInputStream extends InputStream { in = null; } else { try { - System.out.println("Walk " + automaton); + System.out.println("Read consume"); + consumePackets(); + } catch (PGPException e) { + throw new RuntimeException(e); + } + } + } + return r; + } + + @Override + public int read(byte[] b, int off, int len) + throws IOException { + + if (in == null) { + automaton.assertValid(); + return -1; + } + + int r = in.read(b, off, len); + if (r == -1) { + if (in instanceof OpenPgpMessageInputStream) { + in.close(); + in = null; + } else { + try { consumePackets(); } catch (PGPException e) { throw new RuntimeException(e); @@ -343,6 +371,7 @@ public class OpenPgpMessageInputStream extends InputStream { @Override public void close() throws IOException { if (closed) { + automaton.assertValid(); return; } @@ -418,15 +447,15 @@ public class OpenPgpMessageInputStream extends InputStream { public void update(byte b) { /** - for (PGPSignature prepended : prependedSignatures) { - prepended.update(b); - } - for (PGPOnePassSignature ops : onePassSignatures) { - ops.update(b); - } - for (PGPSignature detached : detachedSignatures) { - detached.update(b); - } + for (PGPSignature prepended : prependedSignatures) { + prepended.update(b); + } + for (PGPOnePassSignature ops : onePassSignatures) { + ops.update(b); + } + for (PGPSignature detached : detachedSignatures) { + detached.update(b); + } */ } From ef33d9d584c2bdb0c9a394e425f86e4277fb4154 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Fri, 16 Sep 2022 00:51:49 +0200 Subject: [PATCH 08/80] Implement experimental signature verification (correctness only) --- .../MessageDecryptionStream.java | 278 ------------- .../OpenPgpMessageInputStream.java | 167 +++++++- .../automaton/NestingPDA.java | 339 ---------------- .../MalformedOpenPgpMessageException.java | 15 - .../OpenPgpMessageInputStreamTest.java | 368 +++++++++++++++++- .../PGPDecryptionStreamTest.java | 361 ----------------- .../automaton/NestingPDATest.java | 205 ---------- 7 files changed, 497 insertions(+), 1236 deletions(-) delete mode 100644 pgpainless-core/src/main/java/org/pgpainless/decryption_verification/MessageDecryptionStream.java delete mode 100644 pgpainless-core/src/main/java/org/pgpainless/decryption_verification/automaton/NestingPDA.java delete mode 100644 pgpainless-core/src/test/java/org/pgpainless/decryption_verification/PGPDecryptionStreamTest.java delete mode 100644 pgpainless-core/src/test/java/org/pgpainless/decryption_verification/automaton/NestingPDATest.java diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/MessageDecryptionStream.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/MessageDecryptionStream.java deleted file mode 100644 index 335e9d57..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/MessageDecryptionStream.java +++ /dev/null @@ -1,278 +0,0 @@ -// SPDX-FileCopyrightText: 2022 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.decryption_verification; - -import org.bouncycastle.bcpg.BCPGInputStream; -import org.bouncycastle.bcpg.BCPGOutputStream; -import org.bouncycastle.bcpg.ModDetectionCodePacket; -import org.bouncycastle.bcpg.OnePassSignaturePacket; -import org.bouncycastle.bcpg.Packet; -import org.bouncycastle.bcpg.PacketTags; -import org.bouncycastle.bcpg.PublicKeyEncSessionPacket; -import org.bouncycastle.bcpg.SignaturePacket; -import org.bouncycastle.bcpg.SymmetricKeyEncSessionPacket; -import org.bouncycastle.openpgp.PGPCompressedData; -import org.bouncycastle.openpgp.PGPEncryptedData; -import org.bouncycastle.openpgp.PGPEncryptedDataList; -import org.bouncycastle.openpgp.PGPException; -import org.bouncycastle.openpgp.PGPLiteralData; -import org.bouncycastle.openpgp.PGPOnePassSignatureList; -import org.bouncycastle.openpgp.PGPPBEEncryptedData; -import org.bouncycastle.openpgp.PGPSignatureList; -import org.bouncycastle.openpgp.operator.PBEDataDecryptorFactory; -import org.pgpainless.algorithm.OpenPgpPacket; -import org.pgpainless.decryption_verification.automaton.InputAlphabet; -import org.pgpainless.decryption_verification.automaton.NestingPDA; -import org.pgpainless.exception.MalformedOpenPgpMessageException; -import org.pgpainless.exception.MessageNotIntegrityProtectedException; -import org.pgpainless.exception.MissingDecryptionMethodException; -import org.pgpainless.implementation.ImplementationFactory; -import org.pgpainless.util.Passphrase; - -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.SequenceInputStream; -import java.util.ArrayList; -import java.util.List; -import java.util.NoSuchElementException; -import java.util.Stack; - -public class MessageDecryptionStream extends InputStream { - - private final ConsumerOptions options; - - NestingPDA automaton = new NestingPDA(); - // nested streams, outermost at the bottom of the stack - Stack packetLayers = new Stack<>(); - List pkeskList = new ArrayList<>(); - List skeskList = new ArrayList<>(); - - public MessageDecryptionStream(InputStream inputStream, ConsumerOptions options) - throws IOException, PGPException { - this.options = options; - packetLayers.push(Layer.initial(inputStream)); - walkLayer(); - } - - private void walkLayer() throws PGPException, IOException { - if (packetLayers.isEmpty()) { - return; - } - - // We are currently in the deepest layer - Layer layer = packetLayers.peek(); - BCPGInputStream inputStream = (BCPGInputStream) layer.inputStream; - - loop: while (true) { - if (inputStream.nextPacketTag() == -1) { - popLayer(); - break loop; - } - OpenPgpPacket tag = nextTagOrThrow(inputStream); - switch (tag) { - - case LIT: - automaton.next(InputAlphabet.LiteralData); - PGPLiteralData literalData = new PGPLiteralData(inputStream); - packetLayers.push(Layer.literalMessage(literalData.getDataStream())); - break loop; - - case COMP: - automaton.next(InputAlphabet.CompressedData); - PGPCompressedData compressedData = new PGPCompressedData(inputStream); - inputStream = new BCPGInputStream(compressedData.getDataStream()); - packetLayers.push(Layer.compressedData(inputStream)); - break; - - case OPS: - automaton.next(InputAlphabet.OnePassSignatures); - ByteArrayOutputStream buf = new ByteArrayOutputStream(); - BCPGOutputStream bcpgOut = new BCPGOutputStream(buf); - while (inputStream.nextPacketTag() == PacketTags.ONE_PASS_SIGNATURE || inputStream.nextPacketTag() == PacketTags.MARKER) { - Packet packet = inputStream.readPacket(); - if (packet instanceof OnePassSignaturePacket) { - OnePassSignaturePacket sig = (OnePassSignaturePacket) packet; - sig.encode(bcpgOut); - } - } - PGPOnePassSignatureList onePassSignatures = (PGPOnePassSignatureList) ImplementationFactory.getInstance() - .getPGPObjectFactory(buf.toByteArray()).nextObject(); - break; - - case SIG: - automaton.next(InputAlphabet.Signatures); - - buf = new ByteArrayOutputStream(); - bcpgOut = new BCPGOutputStream(buf); - while (inputStream.nextPacketTag() == PacketTags.SIGNATURE || inputStream.nextPacketTag() == PacketTags.MARKER) { - Packet packet = inputStream.readPacket(); - if (packet instanceof SignaturePacket) { - SignaturePacket sig = (SignaturePacket) packet; - sig.encode(bcpgOut); - } - } - PGPSignatureList signatures = (PGPSignatureList) ImplementationFactory.getInstance() - .getPGPObjectFactory(buf.toByteArray()).nextObject(); - break; - - case PKESK: - PublicKeyEncSessionPacket pkeskPacket = (PublicKeyEncSessionPacket) inputStream.readPacket(); - pkeskList.add(pkeskPacket); - break; - - case SKESK: - SymmetricKeyEncSessionPacket skeskPacket = (SymmetricKeyEncSessionPacket) inputStream.readPacket(); - skeskList.add(skeskPacket); - break; - - case SED: - if (!options.isIgnoreMDCErrors()) { - throw new MessageNotIntegrityProtectedException(); - } - // No break; we continue below! - case SEIPD: - automaton.next(InputAlphabet.EncryptedData); - PGPEncryptedDataList encryptedDataList = assembleEncryptedDataList(inputStream); - - for (PGPEncryptedData encData : encryptedDataList) { - if (encData instanceof PGPPBEEncryptedData) { - PGPPBEEncryptedData skenc = (PGPPBEEncryptedData) encData; - for (Passphrase passphrase : options.getDecryptionPassphrases()) { - PBEDataDecryptorFactory decryptorFactory = ImplementationFactory.getInstance() - .getPBEDataDecryptorFactory(passphrase); - InputStream decryptedIn = skenc.getDataStream(decryptorFactory); - packetLayers.push(Layer.encryptedData(new BCPGInputStream(decryptedIn))); - walkLayer(); - break loop; - } - } - } - throw new MissingDecryptionMethodException("Cannot decrypt message."); - - case MARKER: - inputStream.readPacket(); // discard - break; - - case SK: - case PK: - case SSK: - case PSK: - case TRUST: - case UID: - case UATTR: - throw new MalformedOpenPgpMessageException("OpenPGP packet " + tag + " MUST NOT be part of OpenPGP messages."); - case MOD: - ModDetectionCodePacket modDetectionCodePacket = (ModDetectionCodePacket) inputStream.readPacket(); - break; - case EXP_1: - case EXP_2: - case EXP_3: - case EXP_4: - throw new MalformedOpenPgpMessageException("Experimental packet " + tag + " found inside the message."); - } - } - } - - private PGPEncryptedDataList assembleEncryptedDataList(BCPGInputStream inputStream) - throws IOException { - ByteArrayOutputStream buf = new ByteArrayOutputStream(); - BCPGOutputStream bcpgOut = new BCPGOutputStream(buf); - - for (SymmetricKeyEncSessionPacket skesk : skeskList) { - bcpgOut.write(skesk.getEncoded()); - } - skeskList.clear(); - for (PublicKeyEncSessionPacket pkesk : pkeskList) { - bcpgOut.write(pkesk.getEncoded()); - } - pkeskList.clear(); - - SequenceInputStream sqin = new SequenceInputStream( - new ByteArrayInputStream(buf.toByteArray()), inputStream); - - PGPEncryptedDataList encryptedDataList = (PGPEncryptedDataList) ImplementationFactory.getInstance() - .getPGPObjectFactory(sqin).nextObject(); - return encryptedDataList; - } - - private OpenPgpPacket nextTagOrThrow(BCPGInputStream inputStream) - throws IOException, InvalidOpenPgpPacketException { - try { - return OpenPgpPacket.requireFromTag(inputStream.nextPacketTag()); - } catch (NoSuchElementException e) { - throw new InvalidOpenPgpPacketException(e.getMessage()); - } - } - - private void popLayer() throws MalformedOpenPgpMessageException { - if (packetLayers.pop().isNested) - automaton.next(InputAlphabet.EndOfSequence); - } - - @Override - public int read() throws IOException { - if (packetLayers.isEmpty()) { - automaton.assertValid(); - return -1; - } - - int r = -1; - try { - r = packetLayers.peek().inputStream.read(); - } catch (IOException e) { - } - if (r == -1) { - popLayer(); - try { - walkLayer(); - } catch (PGPException e) { - throw new RuntimeException(e); - } - return read(); - } - return r; - } - - public static class InvalidOpenPgpPacketException extends PGPException { - - public InvalidOpenPgpPacketException(String message) { - super(message); - } - } - - private static class Layer { - InputStream inputStream; - boolean isNested; - - private Layer(InputStream inputStream, boolean isNested) { - this.inputStream = inputStream; - this.isNested = isNested; - } - - static Layer initial(InputStream inputStream) { - BCPGInputStream bcpgIn; - if (inputStream instanceof BCPGInputStream) { - bcpgIn = (BCPGInputStream) inputStream; - } else { - bcpgIn = new BCPGInputStream(inputStream); - } - return new Layer(bcpgIn, true); - } - - static Layer literalMessage(InputStream inputStream) { - return new Layer(inputStream, false); - } - - static Layer compressedData(InputStream inputStream) { - return new Layer(inputStream, true); - } - - static Layer encryptedData(InputStream inputStream) { - return new Layer(inputStream, true); - } - } -} 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 4c6a85c8..8dff7189 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 @@ -18,11 +18,13 @@ import org.bouncycastle.openpgp.PGPPBEEncryptedData; import org.bouncycastle.openpgp.PGPPrivateKey; import org.bouncycastle.openpgp.PGPPublicKey; import org.bouncycastle.openpgp.PGPPublicKeyEncryptedData; +import org.bouncycastle.openpgp.PGPPublicKeyRing; import org.bouncycastle.openpgp.PGPSecretKey; import org.bouncycastle.openpgp.PGPSecretKeyRing; import org.bouncycastle.openpgp.PGPSignature; import org.bouncycastle.openpgp.PGPSignatureList; 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.pgpainless.PGPainless; @@ -38,6 +40,7 @@ import org.pgpainless.implementation.ImplementationFactory; import org.pgpainless.key.info.KeyRingInfo; import org.pgpainless.key.protection.SecretKeyRingProtector; import org.pgpainless.key.protection.UnlockSecretKey; +import org.pgpainless.signature.SignatureUtils; import org.pgpainless.util.Passphrase; import org.pgpainless.util.Tuple; @@ -57,7 +60,7 @@ public class OpenPgpMessageInputStream extends InputStream { private boolean closed = false; - private Signatures signatures = new Signatures(); + private Signatures signatures; public OpenPgpMessageInputStream(InputStream inputStream, ConsumerOptions options) throws IOException, PGPException { @@ -69,6 +72,7 @@ public class OpenPgpMessageInputStream extends InputStream { } this.options = options; + this.signatures = new Signatures(options); this.signatures.addDetachedSignatures(options.getDetachedSignatures()); consumePackets(); @@ -88,8 +92,9 @@ public class OpenPgpMessageInputStream extends InputStream { throws IOException, PGPException { System.out.println("Walk " + automaton); int tag; - loop: while ((tag = bcpgIn.nextPacketTag()) != -1) { + loop: while ((tag = getTag()) != -1) { OpenPgpPacket nextPacket = OpenPgpPacket.requireFromTag(tag); + System.out.println(nextPacket); switch (nextPacket) { // Literal Data - the literal data content is the new input stream @@ -114,9 +119,10 @@ public class OpenPgpMessageInputStream extends InputStream { // Signatures - either prepended to the message, or corresponding to the One Pass Signatures case SIG: + boolean isCorrespondingToOPS = automaton.peekStack() == StackAlphabet.ops; automaton.next(InputAlphabet.Signatures); PGPSignatureList signatureList = readSignatures(); - if (automaton.peekStack() == StackAlphabet.ops) { + if (isCorrespondingToOPS) { signatures.addOnePassCorrespondingSignatures(signatureList); } else { signatures.addPrependedSignatures(signatureList); @@ -246,6 +252,19 @@ public class OpenPgpMessageInputStream extends InputStream { } } + private int getTag() throws IOException { + try { + return bcpgIn.nextPacketTag(); + } catch (IOException e) { + if ("Stream closed".equals(e.getMessage())) { + // ZipInflater Streams sometimes close under our feet -.- + // Therefore we catch resulting IOEs and return -1 instead. + return -1; + } + throw e; + } + } + private List> findPotentialDecryptionKeys(PGPPublicKeyEncryptedData pkesk) { int algorithm = pkesk.getAlgorithm(); List> decryptionKeyCandidates = new ArrayList<>(); @@ -276,8 +295,8 @@ public class OpenPgpMessageInputStream extends InputStream { private PGPOnePassSignatureList readOnePassSignatures() throws IOException { ByteArrayOutputStream buf = new ByteArrayOutputStream(); BCPGOutputStream bcpgOut = new BCPGOutputStream(buf); - int tag = bcpgIn.nextPacketTag(); - while (tag == PacketTags.ONE_PASS_SIGNATURE || tag == PacketTags.MARKER) { + int tag; + while ((tag = getTag()) == PacketTags.ONE_PASS_SIGNATURE || tag == PacketTags.MARKER) { Packet packet = bcpgIn.readPacket(); if (tag == PacketTags.ONE_PASS_SIGNATURE) { OnePassSignaturePacket sigPacket = (OnePassSignaturePacket) packet; @@ -294,13 +313,13 @@ public class OpenPgpMessageInputStream extends InputStream { private PGPSignatureList readSignatures() throws IOException { ByteArrayOutputStream buf = new ByteArrayOutputStream(); BCPGOutputStream bcpgOut = new BCPGOutputStream(buf); - int tag = bcpgIn.nextPacketTag(); + int tag = getTag(); while (tag == PacketTags.SIGNATURE || tag == PacketTags.MARKER) { Packet packet = bcpgIn.readPacket(); if (tag == PacketTags.SIGNATURE) { SignaturePacket sigPacket = (SignaturePacket) packet; sigPacket.encode(bcpgOut); - tag = bcpgIn.nextPacketTag(); + tag = getTag(); } } bcpgOut.close(); @@ -328,6 +347,16 @@ public class OpenPgpMessageInputStream extends InputStream { byte b = (byte) r; signatures.update(b); } else { + in.close(); + in = null; + + try { + consumePackets(); + } catch (PGPException e) { + throw new RuntimeException(e); + } + signatures.finish(); + /* if (in instanceof OpenPgpMessageInputStream) { in.close(); in = null; @@ -335,10 +364,12 @@ public class OpenPgpMessageInputStream extends InputStream { try { System.out.println("Read consume"); consumePackets(); + signatures.finish(); } catch (PGPException e) { throw new RuntimeException(e); } } + */ } return r; } @@ -354,6 +385,16 @@ public class OpenPgpMessageInputStream extends InputStream { int r = in.read(b, off, len); if (r == -1) { + in.close(); + in = null; + + try { + consumePackets(); + } catch (PGPException e) { + throw new RuntimeException(e); + } + signatures.finish(); + /* if (in instanceof OpenPgpMessageInputStream) { in.close(); in = null; @@ -364,6 +405,7 @@ public class OpenPgpMessageInputStream extends InputStream { throw new RuntimeException(e); } } + */ } return r; } @@ -380,6 +422,12 @@ public class OpenPgpMessageInputStream extends InputStream { in = null; } + try { + consumePackets(); + } catch (PGPException e) { + throw new RuntimeException(e); + } + automaton.next(InputAlphabet.EndOfSequence); automaton.assertValid(); closed = true; @@ -418,50 +466,133 @@ public class OpenPgpMessageInputStream extends InputStream { } private static class Signatures { + final ConsumerOptions options; List detachedSignatures = new ArrayList<>(); List prependedSignatures = new ArrayList<>(); List onePassSignatures = new ArrayList<>(); List correspondingSignatures = new ArrayList<>(); + + private Signatures(ConsumerOptions options) { + this.options = options; + } + void addDetachedSignatures(Collection signatures) { + for (PGPSignature signature : signatures) { + long keyId = SignatureUtils.determineIssuerKeyId(signature); + PGPPublicKeyRing certificate = findCertificate(keyId); + initialize(signature, certificate, keyId); + } this.detachedSignatures.addAll(signatures); } void addPrependedSignatures(PGPSignatureList signatures) { + System.out.println("Adding " + signatures.size() + " prepended Signatures"); for (PGPSignature signature : signatures) { + long keyId = SignatureUtils.determineIssuerKeyId(signature); + PGPPublicKeyRing certificate = findCertificate(keyId); + initialize(signature, certificate, keyId); this.prependedSignatures.add(signature); } } void addOnePassSignatures(PGPOnePassSignatureList signatures) { + System.out.println("Adding " + signatures.size() + " OPSs"); for (PGPOnePassSignature ops : signatures) { + PGPPublicKeyRing certificate = findCertificate(ops.getKeyID()); + initialize(ops, certificate); this.onePassSignatures.add(ops); } } void addOnePassCorrespondingSignatures(PGPSignatureList signatures) { + System.out.println("Adding " + signatures.size() + " Corresponding Signatures"); for (PGPSignature signature : signatures) { correspondingSignatures.add(signature); } } + private void initialize(PGPSignature signature, PGPPublicKeyRing certificate, long keyId) { + if (certificate == null) { + // SHIT + return; + } + PGPContentVerifierBuilderProvider verifierProvider = ImplementationFactory.getInstance() + .getPGPContentVerifierBuilderProvider(); + try { + signature.init(verifierProvider, certificate.getPublicKey(keyId)); + } catch (PGPException e) { + throw new RuntimeException(e); + } + } + + private void initialize(PGPOnePassSignature ops, PGPPublicKeyRing certificate) { + if (certificate == null) { + return; + } + PGPContentVerifierBuilderProvider verifierProvider = ImplementationFactory.getInstance() + .getPGPContentVerifierBuilderProvider(); + try { + ops.init(verifierProvider, certificate.getPublicKey(ops.getKeyID())); + } catch (PGPException e) { + throw new RuntimeException(e); + } + } + + private PGPPublicKeyRing findCertificate(long keyId) { + for (PGPPublicKeyRing cert : options.getCertificates()) { + PGPPublicKey verificationKey = cert.getPublicKey(keyId); + if (verificationKey != null) { + return cert; + } + } + return null; // TODO: Missing cert for sig + } + public void update(byte b) { - /** - for (PGPSignature prepended : prependedSignatures) { - prepended.update(b); - } - for (PGPOnePassSignature ops : onePassSignatures) { - ops.update(b); - } - for (PGPSignature detached : detachedSignatures) { - detached.update(b); - } - */ + for (PGPSignature prepended : prependedSignatures) { + prepended.update(b); + } + for (PGPOnePassSignature ops : onePassSignatures) { + ops.update(b); + } + for (PGPSignature detached : detachedSignatures) { + detached.update(b); + } } public void finish() { for (PGPSignature detached : detachedSignatures) { + boolean verified = false; + try { + verified = detached.verify(); + } catch (PGPException e) { + System.out.println(e.getMessage()); + } + System.out.println("Detached Signature by " + Long.toHexString(detached.getKeyID()) + " is " + (verified ? "verified" : "unverified")); + } + for (PGPSignature prepended : prependedSignatures) { + boolean verified = false; + try { + verified = prepended.verify(); + } catch (PGPException e) { + System.out.println(e.getMessage()); + } + System.out.println("Prepended Signature by " + Long.toHexString(prepended.getKeyID()) + " is " + (verified ? "verified" : "unverified")); + } + + + for (int i = 0; i < onePassSignatures.size(); i++) { + PGPOnePassSignature ops = onePassSignatures.get(i); + PGPSignature signature = correspondingSignatures.get(correspondingSignatures.size() - i - 1); + boolean verified = false; + try { + verified = ops.verify(signature); + } catch (PGPException e) { + System.out.println(e.getMessage()); + } + System.out.println("One-Pass-Signature by " + Long.toHexString(ops.getKeyID()) + " is " + (verified ? "verified" : "unverified")); } } } diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/automaton/NestingPDA.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/automaton/NestingPDA.java deleted file mode 100644 index cf5ee674..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/automaton/NestingPDA.java +++ /dev/null @@ -1,339 +0,0 @@ -package org.pgpainless.decryption_verification.automaton; - -import org.pgpainless.exception.MalformedOpenPgpMessageException; - -import java.util.Stack; - -import static org.pgpainless.decryption_verification.automaton.StackAlphabet.msg; -import static org.pgpainless.decryption_verification.automaton.StackAlphabet.ops; -import static org.pgpainless.decryption_verification.automaton.StackAlphabet.terminus; - -/** - * Pushdown Automaton to verify the correct syntax of OpenPGP messages during decryption. - *

- * OpenPGP messages MUST follow certain rules in order to be well-formed. - * Section §11.3. of RFC4880 specifies a formal grammar for OpenPGP messages. - *

- * This grammar was transformed into a pushdown automaton, which is implemented below. - * The automaton only ends up in a valid state ({@link #isValid()} iff the OpenPGP message conformed to the - * grammar. - *

- * There are some specialties with this implementation though: - * Bouncy Castle combines ESKs and Encrypted Data Packets into a single object, so we do not have to - * handle those manually. - *

- * Bouncy Castle further combines OnePassSignatures and Signatures into lists, so instead of pushing multiple - * 'o's onto the stack repeatedly, a sequence of OnePassSignatures causes a single 'o' to be pushed to the stack. - * The same is true for Signatures. - *

- * Therefore, a message is valid, even if the number of OnePassSignatures and Signatures does not match. - * If a message contains at least one OnePassSignature, it is sufficient if there is at least one Signature to - * not cause a {@link MalformedOpenPgpMessageException}. - * - * @see RFC4880 §11.3. OpenPGP Messages - */ -public class NestingPDA { - - /** - * Set of states of the automaton. - * Each state defines its valid transitions in their {@link State#transition(InputAlphabet, NestingPDA)} - * method. - */ - public enum State { - - OpenPgpMessage { - @Override - State transition(InputAlphabet input, NestingPDA automaton) throws MalformedOpenPgpMessageException { - StackAlphabet stackItem = automaton.popStack(); - if (stackItem != msg) { - throw new MalformedOpenPgpMessageException(this, input, stackItem); - } - switch (input) { - - case LiteralData: - return LiteralMessage; - - case Signatures: - automaton.pushStack(msg); - return OpenPgpMessage; - - case OnePassSignatures: - automaton.pushStack(ops); - automaton.pushStack(msg); - return OpenPgpMessage; - - case CompressedData: - return CompressedMessage; - - case EncryptedData: - return EncryptedMessage; - - case EndOfSequence: - default: - throw new MalformedOpenPgpMessageException(this, input, stackItem); - } - } - }, - - LiteralMessage { - @Override - State transition(InputAlphabet input, NestingPDA automaton) throws MalformedOpenPgpMessageException { - StackAlphabet stackItem = automaton.popStack(); - switch (input) { - - case Signatures: - if (stackItem == ops) { - return CorrespondingSignature; - } else { - throw new MalformedOpenPgpMessageException(this, input, stackItem); - } - - case EndOfSequence: - if (stackItem == terminus && automaton.stack.isEmpty()) { - return Valid; - } else { - throw new MalformedOpenPgpMessageException(this, input, stackItem); - } - - case LiteralData: - case OnePassSignatures: - case CompressedData: - case EncryptedData: - default: - throw new MalformedOpenPgpMessageException(this, input, stackItem); - } - } - }, - - CompressedMessage { - @Override - State transition(InputAlphabet input, NestingPDA automaton) throws MalformedOpenPgpMessageException { - StackAlphabet stackItem = automaton.popStack(); - switch (input) { - case Signatures: - if (stackItem == ops) { - return CorrespondingSignature; - } else { - throw new MalformedOpenPgpMessageException(this, input, stackItem); - } - - case EndOfSequence: - if (stackItem == terminus && automaton.stack.isEmpty()) { - return Valid; - } else { - throw new MalformedOpenPgpMessageException(this, input, stackItem); - } - - case LiteralData: - case OnePassSignatures: - case CompressedData: - case EncryptedData: - default: - throw new MalformedOpenPgpMessageException(this, input, stackItem); - } - } - }, - - EncryptedMessage { - @Override - State transition(InputAlphabet input, NestingPDA automaton) throws MalformedOpenPgpMessageException { - StackAlphabet stackItem = automaton.popStack(); - switch (input) { - case Signatures: - if (stackItem == ops) { - return CorrespondingSignature; - } else { - throw new MalformedOpenPgpMessageException(this, input, stackItem); - } - - case EndOfSequence: - if (stackItem == terminus && automaton.stack.isEmpty()) { - return Valid; - } else { - throw new MalformedOpenPgpMessageException(this, input, stackItem); - } - - case LiteralData: - case OnePassSignatures: - case CompressedData: - case EncryptedData: - default: - throw new MalformedOpenPgpMessageException(this, input, stackItem); - } - } - }, - - CorrespondingSignature { - @Override - State transition(InputAlphabet input, NestingPDA automaton) throws MalformedOpenPgpMessageException { - StackAlphabet stackItem = automaton.popStack(); - if (stackItem == terminus && input == InputAlphabet.EndOfSequence && automaton.stack.isEmpty()) { - return Valid; - } else { - throw new MalformedOpenPgpMessageException(this, input, stackItem); - } - } - }, - - Valid { - @Override - State transition(InputAlphabet input, NestingPDA automaton) throws MalformedOpenPgpMessageException { - throw new MalformedOpenPgpMessageException(this, input, null); - } - }, - ; - - /** - * Pop the automatons stack and transition to another state. - * If no valid transition from the current state is available given the popped stack item and input symbol, - * a {@link MalformedOpenPgpMessageException} is thrown. - * Otherwise, the stack is manipulated according to the valid transition and the new state is returned. - * - * @param input input symbol - * @param automaton automaton - * @return new state of the automaton - * @throws MalformedOpenPgpMessageException in case of an illegal input symbol - */ - abstract State transition(InputAlphabet input, NestingPDA automaton) throws MalformedOpenPgpMessageException; - } - - private final Stack stack = new Stack<>(); - private State state; - // Some OpenPGP packets have nested contents (e.g. compressed / encrypted data). - NestingPDA nestedSequence = null; - - public NestingPDA() { - state = State.OpenPgpMessage; - stack.push(terminus); - stack.push(msg); - } - - /** - * Process the next input packet. - * - * @param input input - * @throws MalformedOpenPgpMessageException in case the input packet is illegal here - */ - public void next(InputAlphabet input) throws MalformedOpenPgpMessageException { - _next(input); - } - - /** - * Process the next input packet. - * This method returns true, iff the given input triggered a successful closing of this PDAs nested PDA. - *

- * This is for example the case, if the current packet is a Compressed Data packet which contains a - * valid nested OpenPGP message and the last input was {@link InputAlphabet#EndOfSequence} indicating the - * end of the Compressed Data packet. - *

- * If the input triggered this PDAs nested PDA to close its nested PDA, this method returns false - * in order to prevent this PDA from closing its nested PDA prematurely. - * - * @param input input - * @return true if this just closed its nested sequence, false otherwise - * @throws MalformedOpenPgpMessageException if the input is illegal - */ - private boolean _next(InputAlphabet input) throws MalformedOpenPgpMessageException { - if (nestedSequence != null) { - boolean sequenceInNestedSequenceWasClosed = nestedSequence._next(input); - if (sequenceInNestedSequenceWasClosed) return false; // No need to close out nested sequence too. - } else { - // make a state transition in this automaton - state = state.transition(input, this); - - // If the processed packet contains nested sequence, open nested automaton for it - if (input == InputAlphabet.CompressedData || input == InputAlphabet.EncryptedData) { - nestedSequence = new NestingPDA(); - } - } - - if (input != InputAlphabet.EndOfSequence) { - return false; - } - - // Close nested sequence if needed - boolean nestedIsInnerMost = nestedSequence != null && nestedSequence.isInnerMost(); - if (nestedIsInnerMost) { - if (nestedSequence.isValid()) { - // Close nested sequence - nestedSequence = null; - return true; - } else { - throw new MalformedOpenPgpMessageException("Climbing up nested message validation failed." + - " Automaton for current nesting level is not in valid state: " + nestedSequence.getState() + " " + nestedSequence.stack.peek() + " (Input was " + input + ")"); - } - } - return false; - } - - /** - * Return the current state of the PDA. - * - * @return state - */ - private State getState() { - return state; - } - - /** - * Return true, if the PDA is in a valid state (the OpenPGP message is valid). - * - * @return true if valid, false otherwise - */ - public boolean isValid() { - return getState() == State.Valid && stack.isEmpty(); - } - - public void assertValid() throws MalformedOpenPgpMessageException { - if (!isValid()) { - throw new MalformedOpenPgpMessageException("Pushdown Automaton is not in an acceptable state: " + toString()); - } - } - - /** - * Pop an item from the stack. - * - * @return stack item - */ - private StackAlphabet popStack() { - return stack.pop(); - } - - /** - * Push an item onto the stack. - * - * @param item item - */ - private void pushStack(StackAlphabet item) { - stack.push(item); - } - - /** - * Return true, if this packet sequence has no nested sequence. - * A nested sequence is for example the content of a Compressed Data packet. - * - * @return true if PDA is innermost, false if it has a nested sequence - */ - private boolean isInnerMost() { - return nestedSequence == null; - } - - @Override - public String toString() { - StringBuilder out = new StringBuilder("State: ").append(state) - .append(", Stack (asc.): ").append(stack) - .append('\n'); - if (nestedSequence != null) { - // recursively call toString() on nested PDAs and indent their representation - String nestedToString = nestedSequence.toString(); - String[] lines = nestedToString.split("\n"); - for (int i = 0; i < lines.length; i++) { - String nestedLine = lines[i]; - out.append(i == 0 ? "⤷ " : " ") // indent nested PDA - .append(nestedLine) - .append('\n'); - } - } - return out.toString(); - } -} diff --git a/pgpainless-core/src/main/java/org/pgpainless/exception/MalformedOpenPgpMessageException.java b/pgpainless-core/src/main/java/org/pgpainless/exception/MalformedOpenPgpMessageException.java index 21b6e807..0069209c 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/exception/MalformedOpenPgpMessageException.java +++ b/pgpainless-core/src/main/java/org/pgpainless/exception/MalformedOpenPgpMessageException.java @@ -5,7 +5,6 @@ package org.pgpainless.exception; import org.pgpainless.decryption_verification.automaton.InputAlphabet; -import org.pgpainless.decryption_verification.automaton.NestingPDA; import org.pgpainless.decryption_verification.automaton.PDA; import org.pgpainless.decryption_verification.automaton.StackAlphabet; @@ -21,21 +20,7 @@ public class MalformedOpenPgpMessageException extends RuntimeException { super(message); } - public MalformedOpenPgpMessageException(String message, MalformedOpenPgpMessageException cause) { - super(message, cause); - } - - public MalformedOpenPgpMessageException(NestingPDA.State state, - InputAlphabet input, - StackAlphabet stackItem) { - this("Invalid input: There is no legal transition from state '" + state + "' for input '" + input + "' when '" + stackItem + "' is on top of the stack."); - } - public MalformedOpenPgpMessageException(PDA.State state, InputAlphabet input, StackAlphabet stackItem) { this("Invalid input: There is no legal transition from state '" + state + "' for input '" + input + "' when '" + stackItem + "' is on top of the stack."); } - - public MalformedOpenPgpMessageException(String message, PDA automaton) { - super(message + automaton.toString()); - } } 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 5b42101c..af1fac44 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,87 +1,402 @@ package org.pgpainless.decryption_verification; import org.bouncycastle.bcpg.ArmoredInputStream; +import org.bouncycastle.bcpg.ArmoredOutputStream; +import org.bouncycastle.bcpg.CompressionAlgorithmTags; +import org.bouncycastle.openpgp.PGPCompressedDataGenerator; import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.PGPLiteralData; +import org.bouncycastle.openpgp.PGPLiteralDataGenerator; +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.pgpainless.PGPainless; +import org.pgpainless.algorithm.CompressionAlgorithm; +import org.pgpainless.encryption_signing.EncryptionOptions; +import org.pgpainless.encryption_signing.EncryptionResult; +import org.pgpainless.encryption_signing.EncryptionStream; +import org.pgpainless.encryption_signing.ProducerOptions; +import org.pgpainless.encryption_signing.SigningOptions; 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 static org.pgpainless.decryption_verification.PGPDecryptionStreamTest.COMP; -import static org.pgpainless.decryption_verification.PGPDecryptionStreamTest.COMP_COMP_LIT; -import static org.pgpainless.decryption_verification.PGPDecryptionStreamTest.COMP_LIT; -import static org.pgpainless.decryption_verification.PGPDecryptionStreamTest.KEY; -import static org.pgpainless.decryption_verification.PGPDecryptionStreamTest.LIT; -import static org.pgpainless.decryption_verification.PGPDecryptionStreamTest.LIT_LIT; -import static org.pgpainless.decryption_verification.PGPDecryptionStreamTest.PASSPHRASE; -import static org.pgpainless.decryption_verification.PGPDecryptionStreamTest.PENC_COMP_LIT; -import static org.pgpainless.decryption_verification.PGPDecryptionStreamTest.PLAINTEXT; -import static org.pgpainless.decryption_verification.PGPDecryptionStreamTest.SENC_LIT; -import static org.pgpainless.decryption_verification.PGPDecryptionStreamTest.SIG_LIT; public class OpenPgpMessageInputStreamTest { + + public static final String KEY = "-----BEGIN PGP PRIVATE KEY BLOCK-----\n" + + "Version: PGPainless\n" + + "Comment: DA05 848F 37D4 68E6 F982 C889 7A70 1FC6 904D 3F4C\n" + + "Comment: Alice \n" + + "\n" + + "lFgEYxzSCBYJKwYBBAHaRw8BAQdAeJU8m4GOJb1eQgv/ryilFHRfNLTYFMNqL6zj\n" + + "r0vF7dsAAP42rAtngpJ6dZxoZlJX0Je65zk1VMPeTrXaWfPS2HSKBRGptBxBbGlj\n" + + "ZSA8YWxpY2VAcGdwYWlubGVzcy5vcmc+iI8EExYKAEEFAmMc0ggJEHpwH8aQTT9M\n" + + "FiEE2gWEjzfUaOb5gsiJenAfxpBNP0wCngECmwEFFgIDAQAECwkIBwUVCgkICwKZ\n" + + "AQAApZEBALUXHtvswPZG28YO+16Men6/fpk+scvqpNMnD4ty3IkAAPwK6TuXjNnZ\n" + + "0XuWdnilvLMV23Ai1d5g6em+lwLK5M2SApxdBGMc0ggSCisGAQQBl1UBBQEBB0D8\n" + + "mNUVX8y2MXFaSeFYqOTPFnGT7dgNVdn6yc0UtkkHOgMBCAcAAP9y9OtP4SX9voPb\n" + + "ID2u9PkJKgo4hTB8NK5LouGppdRtEBGriHUEGBYKAB0FAmMc0ggCngECmwwFFgID\n" + + "AQAECwkIBwUVCgkICwAKCRB6cB/GkE0/TAywAQDpZRJS/joFH4+xcwheqWfI7ay/\n" + + "WfojUoGQMYGnUjsgYwEAkceRUsgkqI0SVgYvuglfaQpZ9k2ns1mZGVLkXvu/yQyc\n" + + "WARjHNIIFgkrBgEEAdpHDwEBB0BGN9BybSOrj8B6gim1SjbB/IiqAshlzMDunVkQ\n" + + "X23npQABAJqvjOOY7qhBuTusC5/Q5+25iLrhMn4TI+LXlJHMVNOaE0OI1QQYFgoA\n" + + "fQUCYxzSCAKeAQKbAgUWAgMBAAQLCQgHBRUKCQgLXyAEGRYKAAYFAmMc0ggACgkQ\n" + + "KALh4BJQXl6yTQD/dh0N5228Uwtu7XHy6dmpMRX62cac5tXQ9WaDzpy8STgBAMdn\n" + + "Mq948UOYEhdk/ZY2/hwux/4t+FHvqrXW8ziBe4cLAAoJEHpwH8aQTT9M1hQA/3Ms\n" + + "P3kzoed3VsWu1ZMr7dKEngbc6SoJ2XPayzN0QYJaAQCIY5NcT9mZF97HWV3Vgeum\n" + + "00sWMHXfkW3+nl5OpUZaDA==\n" + + "=THgv\n" + + "-----END PGP PRIVATE KEY BLOCK-----"; + + public static final String PLAINTEXT = "Hello, World!\n"; + public static final String PASSPHRASE = "sw0rdf1sh"; + + public static final String LIT = "" + + "-----BEGIN PGP MESSAGE-----\n" + + "Version: PGPainless\n" + + "\n" + + "yxRiAAAAAABIZWxsbywgV29ybGQhCg==\n" + + "=WGju\n" + + "-----END PGP MESSAGE-----"; + + public static final String LIT_LIT = "" + + "-----BEGIN PGP MESSAGE-----\n" + + "Version: BCPG v1.71\n" + + "\n" + + "yxRiAAAAAABIZWxsbywgV29ybGQhCssUYgAAAAAASGVsbG8sIFdvcmxkIQo=\n" + + "=A91Q\n" + + "-----END PGP MESSAGE-----"; + + public static final String COMP_LIT = "" + + "-----BEGIN PGP MESSAGE-----\n" + + "Version: BCPG v1.71\n" + + "\n" + + "owE7LZLEAAIeqTk5+ToK4flFOSmKXAA=\n" + + "=ZYDg\n" + + "-----END PGP MESSAGE-----"; + + public static final String COMP = "" + + "-----BEGIN PGP MESSAGE-----\n" + + "Version: BCPG v1.71\n" + + "\n" + + "owEDAA==\n" + + "=MDzg\n" + + "-----END PGP MESSAGE-----"; + + public static final String COMP_COMP_LIT = "" + + "-----BEGIN PGP MESSAGE-----\n" + + "Version: BCPG v1.71\n" + + "\n" + + "owEBRwC4/6MDQlpoOTFBWSZTWVuW2KAAAAr3hGAQBABgBABAAIAWBJAAAAggADFM\n" + + "ABNBqBo00N6puqWR+TqInoXQ58XckU4UJBbltigA\n" + + "=K9Zl\n" + + "-----END PGP MESSAGE-----"; + + public static final String SIG_LIT = "" + + "-----BEGIN PGP MESSAGE-----\n" + + "Version: BCPG v1.71\n" + + "\n" + + "iHUEABYKACcFAmMc1i0JECgC4eASUF5eFiEEjN3RiJxCf/TyYOQjKALh4BJQXl4A\n" + + "AHkrAP98uPpqrgIix7epgL7MM1cjXXGSxqbDfXHwgptk1YGQlgD/fw89VGcXwFaI\n" + + "2k7kpXQYy/1BqnovM/jZ3X3mXhhTaAOjATstksQAAh6pOTn5Ogrh+UU5KYpcAA==\n" + + "=WKPn\n" + + "-----END PGP MESSAGE-----"; + + public static final String SENC_LIT = "" + + "-----BEGIN PGP MESSAGE-----\n" + + "Version: PGPainless\n" + + "\n" + + "jA0ECQMCuZ0qHNXWnGhg0j8Bdm1cxV65sYb7jDgb4rRMtdNpQ1dC4UpSYuk9YWS2\n" + + "DpNEijbX8b/P1UOK2kJczNDADMRegZuLEI+dNsBnJjk=\n" + + "=i4Y0\n" + + "-----END PGP MESSAGE-----"; + + public static final String PENC_COMP_LIT = "" + + "-----BEGIN PGP MESSAGE-----\n" + + "Version: PGPainless\n" + + "\n" + + "hF4Dyqa/GWUy6WsSAQdAQ62BwmUt8Iby0+jvrLhMgST79KR/as+dyl0nf1uki2sw\n" + + "Thg1Ojtf0hOyJgcpQ4nP2Q0wYFR0F1sCydaIlTGreYZHlGtybP7/Ml6KNZILTRWP\n" + + "0kYBkGBgK7oQWRIVyoF2POvEP6EX1X8nvQk7O3NysVdRVbnia7gE3AzRYuha4kxs\n" + + "pI6xJkntLMS3K6him1Y9FHINIASFSB+C\n" + + "=5p00\n" + + "-----END PGP MESSAGE-----"; + + public static final String OPS_LIT_SIG = "" + + "-----BEGIN PGP MESSAGE-----\n" + + "Version: PGPainless\n" + + "\n" + + "kA0DAAoWKALh4BJQXl4ByxRiAAAAAABIZWxsbywgV29ybGQhCoh1BAAWCgAnBQJj\n" + + "I3fSCRAoAuHgElBeXhYhBIzd0YicQn/08mDkIygC4eASUF5eAADLOgEA766VyMMv\n" + + "sxfQwQHly3T6ySHSNhYEpoyvdxVqhjBBR+EA/3i6C8lKFPPTh/PvTGbVFOl+eUSV\n" + + "I0w3c+BRY/pO0m4H\n" + + "=tkTV\n" + + "-----END PGP MESSAGE-----"; + + public static void main(String[] args) throws Exception { + // genLIT(); + // genLIT_LIT(); + // genCOMP_LIT(); + // genCOMP(); + // genCOMP_COMP_LIT(); + // genKey(); + // genSIG_LIT(); + // genSENC_LIT(); + // genPENC_COMP_LIT(); + genOPS_LIT_SIG(); + } + + public static void genLIT() throws IOException { + ArmoredOutputStream armorOut = new ArmoredOutputStream(System.out); + PGPLiteralDataGenerator litGen = new PGPLiteralDataGenerator(); + OutputStream litOut = litGen.open(armorOut, PGPLiteralDataGenerator.BINARY, "", PGPLiteralData.NOW, new byte[1 << 9]); + litOut.write(PLAINTEXT.getBytes(StandardCharsets.UTF_8)); + litOut.close(); + armorOut.close(); + } + + public static void genLIT_LIT() throws IOException { + ArmoredOutputStream armorOut = new ArmoredOutputStream(System.out); + PGPLiteralDataGenerator litGen = new PGPLiteralDataGenerator(); + OutputStream litOut = litGen.open(armorOut, PGPLiteralDataGenerator.BINARY, "", PGPLiteralData.NOW, new byte[1 << 9]); + litOut.write(PLAINTEXT.getBytes(StandardCharsets.UTF_8)); + litOut.close(); + + litOut = litGen.open(armorOut, PGPLiteralDataGenerator.BINARY, "", PGPLiteralData.NOW, new byte[1 << 9]); + litOut.write(PLAINTEXT.getBytes(StandardCharsets.UTF_8)); + litOut.close(); + + armorOut.close(); + } + + public static void genCOMP_LIT() throws IOException { + ArmoredOutputStream armorOut = new ArmoredOutputStream(System.out); + PGPCompressedDataGenerator compGen = new PGPCompressedDataGenerator(CompressionAlgorithmTags.ZIP); + OutputStream compOut = compGen.open(armorOut); + PGPLiteralDataGenerator litGen = new PGPLiteralDataGenerator(); + OutputStream litOut = litGen.open(compOut, PGPLiteralDataGenerator.BINARY, "", PGPLiteralData.NOW, new byte[1 << 9]); + litOut.write(PLAINTEXT.getBytes(StandardCharsets.UTF_8)); + litOut.close(); + compOut.close(); + armorOut.close(); + } + + public static void genCOMP() throws IOException { + ArmoredOutputStream armorOut = new ArmoredOutputStream(System.out); + PGPCompressedDataGenerator compGen = new PGPCompressedDataGenerator(CompressionAlgorithmTags.ZIP); + OutputStream compOut = compGen.open(armorOut); + compOut.close(); + armorOut.close(); + } + + public static void genCOMP_COMP_LIT() throws IOException { + ArmoredOutputStream armorOut = new ArmoredOutputStream(System.out); + + PGPCompressedDataGenerator compGen1 = new PGPCompressedDataGenerator(CompressionAlgorithmTags.ZIP); + OutputStream compOut1 = compGen1.open(armorOut); + + PGPCompressedDataGenerator compGen2 = new PGPCompressedDataGenerator(CompressionAlgorithmTags.BZIP2); + OutputStream compOut2 = compGen2.open(compOut1); + + PGPLiteralDataGenerator litGen = new PGPLiteralDataGenerator(); + OutputStream litOut = litGen.open(compOut2, PGPLiteralDataGenerator.BINARY, "", PGPLiteralDataGenerator.NOW, new byte[1 << 9]); + + litOut.write(PLAINTEXT.getBytes(StandardCharsets.UTF_8)); + litOut.close(); + compOut2.close(); + compOut1.close(); + armorOut.close(); + } + + public static void genKey() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, IOException { + System.out.println(PGPainless.asciiArmor( + PGPainless.generateKeyRing().modernKeyRing("Alice ") + )); + } + + public static void genSIG_LIT() throws PGPException, IOException { + PGPSecretKeyRing secretKeys = PGPainless.readKeyRing().secretKeyRing(KEY); + ByteArrayOutputStream msgOut = new ByteArrayOutputStream(); + EncryptionStream signer = PGPainless.encryptAndOrSign() + .onOutputStream(msgOut) + .withOptions( + ProducerOptions.sign( + SigningOptions.get() + .addDetachedSignature(SecretKeyRingProtector.unprotectedKeys(), secretKeys) + ).setAsciiArmor(false) + ); + + Streams.pipeAll(new ByteArrayInputStream(PLAINTEXT.getBytes(StandardCharsets.UTF_8)), signer); + signer.close(); + EncryptionResult result = signer.getResult(); + PGPSignature detachedSignature = result.getDetachedSignatures().get(result.getDetachedSignatures().keySet().iterator().next()).iterator().next(); + + ByteArrayOutputStream out = new ByteArrayOutputStream(); + ArmoredOutputStream armorOut = new ArmoredOutputStream(out); + armorOut.flush(); + detachedSignature.encode(armorOut); + armorOut.write(msgOut.toByteArray()); + armorOut.close(); + + String armored = out.toString(); + System.out.println(armored + .replace("-----BEGIN PGP SIGNATURE-----\n", "-----BEGIN PGP MESSAGE-----\n") + .replace("-----END PGP SIGNATURE-----", "-----END PGP MESSAGE-----")); + } + + public static void genSENC_LIT() throws PGPException, IOException { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + EncryptionStream enc = PGPainless.encryptAndOrSign() + .onOutputStream(out) + .withOptions(ProducerOptions.encrypt(EncryptionOptions.get() + .addPassphrase(Passphrase.fromPassword(PASSPHRASE))) + .overrideCompressionAlgorithm(CompressionAlgorithm.UNCOMPRESSED)); + enc.write(PLAINTEXT.getBytes(StandardCharsets.UTF_8)); + enc.close(); + + System.out.println(out); + } + + public static void genPENC_COMP_LIT() throws IOException, PGPException { + PGPSecretKeyRing secretKeys = PGPainless.readKeyRing().secretKeyRing(KEY); + PGPPublicKeyRing cert = PGPainless.extractCertificate(secretKeys); + + ByteArrayOutputStream out = new ByteArrayOutputStream(); + EncryptionStream enc = PGPainless.encryptAndOrSign() + .onOutputStream(out) + .withOptions(ProducerOptions.encrypt(EncryptionOptions.get() + .addRecipient(cert)) + .overrideCompressionAlgorithm(CompressionAlgorithm.ZLIB)); + + Streams.pipeAll(new ByteArrayInputStream(PLAINTEXT.getBytes(StandardCharsets.UTF_8)), enc); + enc.close(); + + System.out.println(out); + } + + public static void genOPS_LIT_SIG() throws PGPException, IOException { + PGPSecretKeyRing secretKeys = PGPainless.readKeyRing().secretKeyRing(KEY); + + ByteArrayOutputStream out = new ByteArrayOutputStream(); + EncryptionStream enc = PGPainless.encryptAndOrSign() + .onOutputStream(out) + .withOptions(ProducerOptions.sign(SigningOptions.get() + .addSignature(SecretKeyRingProtector.unprotectedKeys(), secretKeys)) + .overrideCompressionAlgorithm(CompressionAlgorithm.UNCOMPRESSED)); + Streams.pipeAll(new ByteArrayInputStream(PLAINTEXT.getBytes(StandardCharsets.UTF_8)), enc); + enc.close(); + + System.out.println(out); + } + @Test public void testProcessLIT() throws IOException, PGPException { - String plain = process(LIT, ConsumerOptions.get()); + String plain = processReadBuffered(LIT, ConsumerOptions.get()); + assertEquals(PLAINTEXT, plain); + + plain = processReadSequential(LIT, ConsumerOptions.get()); assertEquals(PLAINTEXT, plain); } @Test public void testProcessLIT_LIT_fails() { assertThrows(MalformedOpenPgpMessageException.class, - () -> process(LIT_LIT, ConsumerOptions.get())); + () -> processReadBuffered(LIT_LIT, ConsumerOptions.get())); + + assertThrows(MalformedOpenPgpMessageException.class, + () -> processReadSequential(LIT_LIT, ConsumerOptions.get())); } @Test public void testProcessCOMP_LIT() throws PGPException, IOException { - String plain = process(COMP_LIT, ConsumerOptions.get()); + String plain = processReadBuffered(COMP_LIT, ConsumerOptions.get()); + assertEquals(PLAINTEXT, plain); + + plain = processReadSequential(COMP_LIT, ConsumerOptions.get()); assertEquals(PLAINTEXT, plain); } @Test public void testProcessCOMP_fails() { assertThrows(MalformedOpenPgpMessageException.class, - () -> process(COMP, ConsumerOptions.get())); + () -> processReadBuffered(COMP, ConsumerOptions.get())); + + assertThrows(MalformedOpenPgpMessageException.class, + () -> processReadSequential(COMP, ConsumerOptions.get())); } @Test public void testProcessCOMP_COMP_LIT() throws PGPException, IOException { - String plain = process(COMP_COMP_LIT, ConsumerOptions.get()); + 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 { - String plain = process(SIG_LIT, ConsumerOptions.get()); + 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() + .addVerificationCert(cert)); assertEquals(PLAINTEXT, plain); } @Test public void testProcessSENC_LIT() throws PGPException, IOException { - String plain = process(SENC_LIT, ConsumerOptions.get().addDecryptionPassphrase(Passphrase.fromPassword(PASSPHRASE))); + String plain = processReadBuffered(SENC_LIT, ConsumerOptions.get().addDecryptionPassphrase(Passphrase.fromPassword(PASSPHRASE))); + assertEquals(PLAINTEXT, plain); + + plain = processReadSequential(SENC_LIT, ConsumerOptions.get().addDecryptionPassphrase(Passphrase.fromPassword(PASSPHRASE))); assertEquals(PLAINTEXT, plain); } @Test public void testProcessPENC_COMP_LIT() throws IOException, PGPException { PGPSecretKeyRing secretKeys = PGPainless.readKeyRing().secretKeyRing(KEY); - String plain = process(PENC_COMP_LIT, ConsumerOptions.get() + String plain = processReadBuffered(PENC_COMP_LIT, ConsumerOptions.get() + .addDecryptionKey(secretKeys)); + assertEquals(PLAINTEXT, plain); + + plain = processReadSequential(PENC_COMP_LIT, ConsumerOptions.get() .addDecryptionKey(secretKeys)); assertEquals(PLAINTEXT, plain); } - private String process(String armoredMessage, ConsumerOptions options) throws PGPException, IOException { + @Test + public void testProcessOPS_LIT_SIG() 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() + .addVerificationCert(cert)); + assertEquals(PLAINTEXT, plain); + } + + private String processReadBuffered(String armoredMessage, ConsumerOptions options) throws PGPException, IOException { OpenPgpMessageInputStream in = get(armoredMessage, options); ByteArrayOutputStream out = new ByteArrayOutputStream(); Streams.pipeAll(in, out); @@ -89,6 +404,19 @@ public class OpenPgpMessageInputStreamTest { return out.toString(); } + private String processReadSequential(String armoredMessage, ConsumerOptions options) throws PGPException, IOException { + OpenPgpMessageInputStream in = get(armoredMessage, options); + ByteArrayOutputStream out = new ByteArrayOutputStream(); + + int r; + while ((r = in.read()) != -1) { + out.write(r); + } + + in.close(); + return out.toString(); + } + private OpenPgpMessageInputStream get(String armored, ConsumerOptions options) throws IOException, PGPException { ByteArrayInputStream bytesIn = new ByteArrayInputStream(armored.getBytes(StandardCharsets.UTF_8)); ArmoredInputStream armorIn = ArmoredInputStreamFactory.get(bytesIn); diff --git a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/PGPDecryptionStreamTest.java b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/PGPDecryptionStreamTest.java deleted file mode 100644 index 8da44ec8..00000000 --- a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/PGPDecryptionStreamTest.java +++ /dev/null @@ -1,361 +0,0 @@ -package org.pgpainless.decryption_verification; - -import org.bouncycastle.bcpg.ArmoredInputStream; -import org.bouncycastle.bcpg.ArmoredOutputStream; -import org.bouncycastle.bcpg.CompressionAlgorithmTags; -import org.bouncycastle.openpgp.PGPCompressedDataGenerator; -import org.bouncycastle.openpgp.PGPException; -import org.bouncycastle.openpgp.PGPLiteralData; -import org.bouncycastle.openpgp.PGPLiteralDataGenerator; -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.pgpainless.PGPainless; -import org.pgpainless.algorithm.CompressionAlgorithm; -import org.pgpainless.encryption_signing.EncryptionOptions; -import org.pgpainless.encryption_signing.EncryptionResult; -import org.pgpainless.encryption_signing.EncryptionStream; -import org.pgpainless.encryption_signing.ProducerOptions; -import org.pgpainless.encryption_signing.SigningOptions; -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; - -public class PGPDecryptionStreamTest { - - public static final String KEY = "-----BEGIN PGP PRIVATE KEY BLOCK-----\n" + - "Version: PGPainless\n" + - "Comment: DA05 848F 37D4 68E6 F982 C889 7A70 1FC6 904D 3F4C\n" + - "Comment: Alice \n" + - "\n" + - "lFgEYxzSCBYJKwYBBAHaRw8BAQdAeJU8m4GOJb1eQgv/ryilFHRfNLTYFMNqL6zj\n" + - "r0vF7dsAAP42rAtngpJ6dZxoZlJX0Je65zk1VMPeTrXaWfPS2HSKBRGptBxBbGlj\n" + - "ZSA8YWxpY2VAcGdwYWlubGVzcy5vcmc+iI8EExYKAEEFAmMc0ggJEHpwH8aQTT9M\n" + - "FiEE2gWEjzfUaOb5gsiJenAfxpBNP0wCngECmwEFFgIDAQAECwkIBwUVCgkICwKZ\n" + - "AQAApZEBALUXHtvswPZG28YO+16Men6/fpk+scvqpNMnD4ty3IkAAPwK6TuXjNnZ\n" + - "0XuWdnilvLMV23Ai1d5g6em+lwLK5M2SApxdBGMc0ggSCisGAQQBl1UBBQEBB0D8\n" + - "mNUVX8y2MXFaSeFYqOTPFnGT7dgNVdn6yc0UtkkHOgMBCAcAAP9y9OtP4SX9voPb\n" + - "ID2u9PkJKgo4hTB8NK5LouGppdRtEBGriHUEGBYKAB0FAmMc0ggCngECmwwFFgID\n" + - "AQAECwkIBwUVCgkICwAKCRB6cB/GkE0/TAywAQDpZRJS/joFH4+xcwheqWfI7ay/\n" + - "WfojUoGQMYGnUjsgYwEAkceRUsgkqI0SVgYvuglfaQpZ9k2ns1mZGVLkXvu/yQyc\n" + - "WARjHNIIFgkrBgEEAdpHDwEBB0BGN9BybSOrj8B6gim1SjbB/IiqAshlzMDunVkQ\n" + - "X23npQABAJqvjOOY7qhBuTusC5/Q5+25iLrhMn4TI+LXlJHMVNOaE0OI1QQYFgoA\n" + - "fQUCYxzSCAKeAQKbAgUWAgMBAAQLCQgHBRUKCQgLXyAEGRYKAAYFAmMc0ggACgkQ\n" + - "KALh4BJQXl6yTQD/dh0N5228Uwtu7XHy6dmpMRX62cac5tXQ9WaDzpy8STgBAMdn\n" + - "Mq948UOYEhdk/ZY2/hwux/4t+FHvqrXW8ziBe4cLAAoJEHpwH8aQTT9M1hQA/3Ms\n" + - "P3kzoed3VsWu1ZMr7dKEngbc6SoJ2XPayzN0QYJaAQCIY5NcT9mZF97HWV3Vgeum\n" + - "00sWMHXfkW3+nl5OpUZaDA==\n" + - "=THgv\n" + - "-----END PGP PRIVATE KEY BLOCK-----"; - - public static final String PLAINTEXT = "Hello, World!\n"; - public static final String PASSPHRASE = "sw0rdf1sh"; - - public static final String LIT = "" + - "-----BEGIN PGP MESSAGE-----\n" + - "Version: PGPainless\n" + - "\n" + - "yxRiAAAAAABIZWxsbywgV29ybGQhCg==\n" + - "=WGju\n" + - "-----END PGP MESSAGE-----"; - - public static final String LIT_LIT = "" + - "-----BEGIN PGP MESSAGE-----\n" + - "Version: BCPG v1.71\n" + - "\n" + - "yxRiAAAAAABIZWxsbywgV29ybGQhCssUYgAAAAAASGVsbG8sIFdvcmxkIQo=\n" + - "=A91Q\n" + - "-----END PGP MESSAGE-----"; - - public static final String COMP_LIT = "" + - "-----BEGIN PGP MESSAGE-----\n" + - "Version: BCPG v1.71\n" + - "\n" + - "owE7LZLEAAIeqTk5+ToK4flFOSmKXAA=\n" + - "=ZYDg\n" + - "-----END PGP MESSAGE-----"; - - public static final String COMP = "" + - "-----BEGIN PGP MESSAGE-----\n" + - "Version: BCPG v1.71\n" + - "\n" + - "owEDAA==\n" + - "=MDzg\n" + - "-----END PGP MESSAGE-----"; - - public static final String COMP_COMP_LIT = "" + - "-----BEGIN PGP MESSAGE-----\n" + - "Version: BCPG v1.71\n" + - "\n" + - "owEBRwC4/6MDQlpoOTFBWSZTWVuW2KAAAAr3hGAQBABgBABAAIAWBJAAAAggADFM\n" + - "ABNBqBo00N6puqWR+TqInoXQ58XckU4UJBbltigA\n" + - "=K9Zl\n" + - "-----END PGP MESSAGE-----"; - - public static final String SIG_LIT = "" + - "-----BEGIN PGP MESSAGE-----\n" + - "Version: BCPG v1.71\n" + - "\n" + - "iHUEABYKACcFAmMc1i0JECgC4eASUF5eFiEEjN3RiJxCf/TyYOQjKALh4BJQXl4A\n" + - "AHkrAP98uPpqrgIix7epgL7MM1cjXXGSxqbDfXHwgptk1YGQlgD/fw89VGcXwFaI\n" + - "2k7kpXQYy/1BqnovM/jZ3X3mXhhTaAOjATstksQAAh6pOTn5Ogrh+UU5KYpcAA==\n" + - "=WKPn\n" + - "-----END PGP MESSAGE-----"; - - public static final String SENC_LIT = "" + - "-----BEGIN PGP MESSAGE-----\n" + - "Version: PGPainless\n" + - "\n" + - "jA0ECQMCuZ0qHNXWnGhg0j8Bdm1cxV65sYb7jDgb4rRMtdNpQ1dC4UpSYuk9YWS2\n" + - "DpNEijbX8b/P1UOK2kJczNDADMRegZuLEI+dNsBnJjk=\n" + - "=i4Y0\n" + - "-----END PGP MESSAGE-----"; - - public static final String PENC_COMP_LIT = "" + - "-----BEGIN PGP MESSAGE-----\n" + - "Version: PGPainless\n" + - "\n" + - "hF4Dyqa/GWUy6WsSAQdAQ62BwmUt8Iby0+jvrLhMgST79KR/as+dyl0nf1uki2sw\n" + - "Thg1Ojtf0hOyJgcpQ4nP2Q0wYFR0F1sCydaIlTGreYZHlGtybP7/Ml6KNZILTRWP\n" + - "0kYBkGBgK7oQWRIVyoF2POvEP6EX1X8nvQk7O3NysVdRVbnia7gE3AzRYuha4kxs\n" + - "pI6xJkntLMS3K6him1Y9FHINIASFSB+C\n" + - "=5p00\n" + - "-----END PGP MESSAGE-----"; - - @Test - public void genLIT() throws IOException { - ArmoredOutputStream armorOut = new ArmoredOutputStream(System.out); - PGPLiteralDataGenerator litGen = new PGPLiteralDataGenerator(); - OutputStream litOut = litGen.open(armorOut, PGPLiteralDataGenerator.BINARY, "", PGPLiteralData.NOW, new byte[1 << 9]); - litOut.write(PLAINTEXT.getBytes(StandardCharsets.UTF_8)); - litOut.close(); - armorOut.close(); - } - - @Test - public void processLIT() throws IOException, PGPException { - ByteArrayInputStream bytesIn = new ByteArrayInputStream(LIT.getBytes(StandardCharsets.UTF_8)); - ArmoredInputStream armorIn = ArmoredInputStreamFactory.get(bytesIn); - MessageDecryptionStream decIn = new MessageDecryptionStream(armorIn, ConsumerOptions.get()); - - ByteArrayOutputStream out = new ByteArrayOutputStream(); - Streams.pipeAll(decIn, out); - assertEquals(PLAINTEXT, out.toString()); - armorIn.close(); - } - - @Test - public void getLIT_LIT() throws IOException { - ArmoredOutputStream armorOut = new ArmoredOutputStream(System.out); - PGPLiteralDataGenerator litGen = new PGPLiteralDataGenerator(); - OutputStream litOut = litGen.open(armorOut, PGPLiteralDataGenerator.BINARY, "", PGPLiteralData.NOW, new byte[1 << 9]); - litOut.write(PLAINTEXT.getBytes(StandardCharsets.UTF_8)); - litOut.close(); - - litOut = litGen.open(armorOut, PGPLiteralDataGenerator.BINARY, "", PGPLiteralData.NOW, new byte[1 << 9]); - litOut.write(PLAINTEXT.getBytes(StandardCharsets.UTF_8)); - litOut.close(); - - armorOut.close(); - } - - @Test - public void processLIT_LIT() throws IOException, PGPException { - ByteArrayInputStream bytesIn = new ByteArrayInputStream(LIT_LIT.getBytes(StandardCharsets.UTF_8)); - ArmoredInputStream armorIn = ArmoredInputStreamFactory.get(bytesIn); - MessageDecryptionStream decIn = new MessageDecryptionStream(armorIn, ConsumerOptions.get()); - - ByteArrayOutputStream out = new ByteArrayOutputStream(); - assertThrows(MalformedOpenPgpMessageException.class, () -> Streams.pipeAll(decIn, out)); - } - - @Test - public void genCOMP_LIT() throws IOException { - ArmoredOutputStream armorOut = new ArmoredOutputStream(System.out); - PGPCompressedDataGenerator compGen = new PGPCompressedDataGenerator(CompressionAlgorithmTags.ZIP); - OutputStream compOut = compGen.open(armorOut); - PGPLiteralDataGenerator litGen = new PGPLiteralDataGenerator(); - OutputStream litOut = litGen.open(compOut, PGPLiteralDataGenerator.BINARY, "", PGPLiteralData.NOW, new byte[1 << 9]); - litOut.write(PLAINTEXT.getBytes(StandardCharsets.UTF_8)); - litOut.close(); - compOut.close(); - armorOut.close(); - } - - @Test - public void processCOMP_LIT() throws IOException, PGPException { - ByteArrayInputStream bytesIn = new ByteArrayInputStream(COMP_LIT.getBytes(StandardCharsets.UTF_8)); - ArmoredInputStream armorIn = ArmoredInputStreamFactory.get(bytesIn); - MessageDecryptionStream decIn = new MessageDecryptionStream(armorIn, ConsumerOptions.get()); - - ByteArrayOutputStream out = new ByteArrayOutputStream(); - Streams.pipeAll(decIn, out); - decIn.close(); - armorIn.close(); - - assertEquals(PLAINTEXT, out.toString()); - } - - @Test - public void genCOMP() throws IOException { - ArmoredOutputStream armorOut = new ArmoredOutputStream(System.out); - PGPCompressedDataGenerator compGen = new PGPCompressedDataGenerator(CompressionAlgorithmTags.ZIP); - OutputStream compOut = compGen.open(armorOut); - compOut.close(); - armorOut.close(); - } - - @Test - public void processCOMP() throws IOException { - ByteArrayInputStream bytesIn = new ByteArrayInputStream(COMP.getBytes(StandardCharsets.UTF_8)); - ArmoredInputStream armorIn = ArmoredInputStreamFactory.get(bytesIn); - assertThrows(MalformedOpenPgpMessageException.class, () -> { - MessageDecryptionStream decIn = new MessageDecryptionStream(armorIn, ConsumerOptions.get()); - Streams.drain(decIn); - }); - } - - @Test - public void genCOMP_COMP_LIT() throws IOException { - ArmoredOutputStream armorOut = new ArmoredOutputStream(System.out); - - PGPCompressedDataGenerator compGen1 = new PGPCompressedDataGenerator(CompressionAlgorithmTags.ZIP); - OutputStream compOut1 = compGen1.open(armorOut); - - PGPCompressedDataGenerator compGen2 = new PGPCompressedDataGenerator(CompressionAlgorithmTags.BZIP2); - OutputStream compOut2 = compGen2.open(compOut1); - - PGPLiteralDataGenerator litGen = new PGPLiteralDataGenerator(); - OutputStream litOut = litGen.open(compOut2, PGPLiteralDataGenerator.BINARY, "", PGPLiteralDataGenerator.NOW, new byte[1 << 9]); - - litOut.write(PLAINTEXT.getBytes(StandardCharsets.UTF_8)); - litOut.close(); - compOut2.close(); - compOut1.close(); - armorOut.close(); - } - - @Test - public void processCOMP_COMP_LIT() throws PGPException, IOException { - ByteArrayInputStream bytesIn = new ByteArrayInputStream(COMP_COMP_LIT.getBytes(StandardCharsets.UTF_8)); - ArmoredInputStream armorIn = ArmoredInputStreamFactory.get(bytesIn); - MessageDecryptionStream decIn = new MessageDecryptionStream(armorIn, ConsumerOptions.get()); - - ByteArrayOutputStream out = new ByteArrayOutputStream(); - Streams.pipeAll(decIn, out); - decIn.close(); - - assertEquals(PLAINTEXT, out.toString()); - } - - @Test - public void genKey() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, IOException { - System.out.println(PGPainless.asciiArmor( - PGPainless.generateKeyRing().modernKeyRing("Alice ") - )); - } - - @Test - public void genSIG_LIT() throws PGPException, IOException { - PGPSecretKeyRing secretKeys = PGPainless.readKeyRing().secretKeyRing(KEY); - ByteArrayOutputStream msgOut = new ByteArrayOutputStream(); - EncryptionStream signer = PGPainless.encryptAndOrSign() - .onOutputStream(msgOut) - .withOptions( - ProducerOptions.sign( - SigningOptions.get() - .addDetachedSignature(SecretKeyRingProtector.unprotectedKeys(), secretKeys) - ).setAsciiArmor(false) - ); - - Streams.pipeAll(new ByteArrayInputStream(PLAINTEXT.getBytes(StandardCharsets.UTF_8)), signer); - signer.close(); - EncryptionResult result = signer.getResult(); - PGPSignature detachedSignature = result.getDetachedSignatures().get(result.getDetachedSignatures().keySet().iterator().next()).iterator().next(); - - ByteArrayOutputStream out = new ByteArrayOutputStream(); - ArmoredOutputStream armorOut = new ArmoredOutputStream(out); - armorOut.flush(); - detachedSignature.encode(armorOut); - armorOut.write(msgOut.toByteArray()); - armorOut.close(); - - String armored = out.toString(); - System.out.println(armored - .replace("-----BEGIN PGP SIGNATURE-----\n", "-----BEGIN PGP MESSAGE-----\n") - .replace("-----END PGP SIGNATURE-----", "-----END PGP MESSAGE-----")); - } - - @Test - public void processSIG_LIT() throws IOException, PGPException { - ByteArrayInputStream bytesIn = new ByteArrayInputStream(SIG_LIT.getBytes(StandardCharsets.UTF_8)); - ArmoredInputStream armorIn = ArmoredInputStreamFactory.get(bytesIn); - MessageDecryptionStream decIn = new MessageDecryptionStream(armorIn, ConsumerOptions.get()); - - ByteArrayOutputStream out = new ByteArrayOutputStream(); - Streams.pipeAll(decIn, out); - decIn.close(); - - System.out.println(out); - } - - @Test - public void genSENC_LIT() throws PGPException, IOException { - ByteArrayOutputStream out = new ByteArrayOutputStream(); - EncryptionStream enc = PGPainless.encryptAndOrSign() - .onOutputStream(out) - .withOptions(ProducerOptions.encrypt(EncryptionOptions.get() - .addPassphrase(Passphrase.fromPassword(PASSPHRASE))) - .overrideCompressionAlgorithm(CompressionAlgorithm.UNCOMPRESSED)); - enc.write(PLAINTEXT.getBytes(StandardCharsets.UTF_8)); - enc.close(); - - System.out.println(out); - } - - @Test - public void processSENC_LIT() throws IOException, PGPException { - ByteArrayInputStream bytesIn = new ByteArrayInputStream(SENC_LIT.getBytes(StandardCharsets.UTF_8)); - ArmoredInputStream armorIn = ArmoredInputStreamFactory.get(bytesIn); - MessageDecryptionStream decIn = new MessageDecryptionStream(armorIn, ConsumerOptions.get() - .addDecryptionPassphrase(Passphrase.fromPassword(PASSPHRASE))); - - ByteArrayOutputStream out = new ByteArrayOutputStream(); - Streams.pipeAll(decIn, out); - decIn.close(); - - System.out.println(out); - } - - @Test - public void genPENC_COMP_LIT() throws IOException, PGPException { - PGPSecretKeyRing secretKeys = PGPainless.readKeyRing().secretKeyRing(KEY); - PGPPublicKeyRing cert = PGPainless.extractCertificate(secretKeys); - - ByteArrayOutputStream out = new ByteArrayOutputStream(); - EncryptionStream enc = PGPainless.encryptAndOrSign() - .onOutputStream(out) - .withOptions(ProducerOptions.encrypt(EncryptionOptions.get() - .addRecipient(cert)) - .overrideCompressionAlgorithm(CompressionAlgorithm.ZLIB)); - - Streams.pipeAll(new ByteArrayInputStream(PLAINTEXT.getBytes(StandardCharsets.UTF_8)), enc); - enc.close(); - - System.out.println(out); - } -} diff --git a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/automaton/NestingPDATest.java b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/automaton/NestingPDATest.java deleted file mode 100644 index 8c1c4921..00000000 --- a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/automaton/NestingPDATest.java +++ /dev/null @@ -1,205 +0,0 @@ -// SPDX-FileCopyrightText: 2022 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.decryption_verification.automaton; - -import org.junit.jupiter.api.Test; -import org.pgpainless.exception.MalformedOpenPgpMessageException; - -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.assertTrue; - -public class NestingPDATest { - - /** - * MSG is valid. - * - * @throws MalformedOpenPgpMessageException fail - */ - @Test - public void testSimpleLiteralMessageIsValid() throws MalformedOpenPgpMessageException { - NestingPDA automaton = new NestingPDA(); - automaton.next(InputAlphabet.LiteralData); - automaton.next(InputAlphabet.EndOfSequence); - - assertTrue(automaton.isValid()); - } - - /** - * OPS MSG SIG is valid. - * - * @throws MalformedOpenPgpMessageException fail - */ - @Test - public void testSimpleOpsSignedMesssageIsValid() throws MalformedOpenPgpMessageException { - NestingPDA automaton = new NestingPDA(); - automaton.next(InputAlphabet.OnePassSignatures); - automaton.next(InputAlphabet.LiteralData); - automaton.next(InputAlphabet.Signatures); - automaton.next(InputAlphabet.EndOfSequence); - - assertTrue(automaton.isValid()); - } - - /** - * SIG MSG is valid. - * - * @throws MalformedOpenPgpMessageException fail - */ - @Test - public void testSimplePrependSignedMessageIsValid() throws MalformedOpenPgpMessageException { - NestingPDA automaton = new NestingPDA(); - automaton.next(InputAlphabet.Signatures); - automaton.next(InputAlphabet.LiteralData); - automaton.next(InputAlphabet.EndOfSequence); - - assertTrue(automaton.isValid()); - } - - /** - * OPS COMP(MSG) SIG is valid. - * - * @throws MalformedOpenPgpMessageException fail - */ - @Test - public void testOPSSignedCompressedMessageIsValid() throws MalformedOpenPgpMessageException { - NestingPDA automaton = new NestingPDA(); - automaton.next(InputAlphabet.OnePassSignatures); - automaton.next(InputAlphabet.CompressedData); - automaton.next(InputAlphabet.LiteralData); - automaton.next(InputAlphabet.EndOfSequence); - automaton.next(InputAlphabet.Signatures); - automaton.next(InputAlphabet.EndOfSequence); - - assertTrue(automaton.isValid()); - } - - /** - * OPS ENC(COMP(COMP(MSG))) SIG is valid. - * - * @throws MalformedOpenPgpMessageException fail - */ - @Test - public void testOpsSignedEncryptedCompressedCompressedMessageIsValid() throws MalformedOpenPgpMessageException { - NestingPDA automaton = new NestingPDA(); - automaton.next(InputAlphabet.OnePassSignatures); - automaton.next(InputAlphabet.EncryptedData); - automaton.next(InputAlphabet.CompressedData); - automaton.next(InputAlphabet.CompressedData); - - automaton.next(InputAlphabet.LiteralData); - - automaton.next(InputAlphabet.EndOfSequence); - automaton.next(InputAlphabet.EndOfSequence); - automaton.next(InputAlphabet.EndOfSequence); - automaton.next(InputAlphabet.Signatures); - automaton.next(InputAlphabet.EndOfSequence); - - assertTrue(automaton.isValid()); - } - - /** - * MSG SIG is invalid. - * - * @throws MalformedOpenPgpMessageException fail - */ - @Test - public void testLiteralPlusSigsFails() throws MalformedOpenPgpMessageException { - NestingPDA automaton = new NestingPDA(); - automaton.next(InputAlphabet.LiteralData); - assertThrows(MalformedOpenPgpMessageException.class, - () -> automaton.next(InputAlphabet.Signatures)); - } - - /** - * MSG MSG is invalid. - * - * @throws MalformedOpenPgpMessageException fail - */ - @Test - public void testTwoLiteralDataPacketsFails() throws MalformedOpenPgpMessageException { - NestingPDA automaton = new NestingPDA(); - automaton.next(InputAlphabet.LiteralData); - assertThrows(MalformedOpenPgpMessageException.class, - () -> automaton.next(InputAlphabet.LiteralData)); - } - - /** - * OPS COMP(MSG MSG) SIG is invalid (two literal packets are illegal). - * - * @throws MalformedOpenPgpMessageException fail - */ - @Test - public void testOPSSignedMessageWithTwoLiteralDataPacketsFails() throws MalformedOpenPgpMessageException { - NestingPDA automaton = new NestingPDA(); - automaton.next(InputAlphabet.OnePassSignatures); - automaton.next(InputAlphabet.CompressedData); - automaton.next(InputAlphabet.LiteralData); - assertThrows(MalformedOpenPgpMessageException.class, - () -> automaton.next(InputAlphabet.LiteralData)); - } - - /** - * OPS COMP(MSG) MSG SIG is invalid. - * - * @throws MalformedOpenPgpMessageException fail - */ - @Test - public void testOPSSignedMessageWithTwoLiteralDataPacketsFails2() throws MalformedOpenPgpMessageException { - NestingPDA automaton = new NestingPDA(); - automaton.next(InputAlphabet.OnePassSignatures); - automaton.next(InputAlphabet.CompressedData); - automaton.next(InputAlphabet.LiteralData); - automaton.next(InputAlphabet.EndOfSequence); - assertThrows(MalformedOpenPgpMessageException.class, - () -> automaton.next(InputAlphabet.LiteralData)); - } - - /** - * OPS COMP(MSG SIG) is invalid (MSG SIG does not form valid nested message). - * - * @throws MalformedOpenPgpMessageException fail - */ - @Test - public void testCorrespondingSignaturesOfOpsSignedMessageAreLayerFurtherDownFails() throws MalformedOpenPgpMessageException { - NestingPDA automaton = new NestingPDA(); - automaton.next(InputAlphabet.OnePassSignatures); - automaton.next(InputAlphabet.CompressedData); - automaton.next(InputAlphabet.LiteralData); - assertThrows(MalformedOpenPgpMessageException.class, - () -> automaton.next(InputAlphabet.Signatures)); - } - - /** - * Empty COMP is invalid. - */ - @Test - public void testEmptyCompressedDataIsInvalid() throws MalformedOpenPgpMessageException { - NestingPDA automaton = new NestingPDA(); - automaton.next(InputAlphabet.CompressedData); - assertThrows(MalformedOpenPgpMessageException.class, - () -> automaton.next(InputAlphabet.EndOfSequence)); - } - - @Test - public void testOPSSignedEncryptedCompressedOPSSignedMessageIsValid() throws MalformedOpenPgpMessageException { - NestingPDA automaton = new NestingPDA(); - automaton.next(InputAlphabet.OnePassSignatures); - - automaton.next(InputAlphabet.EncryptedData); - automaton.next(InputAlphabet.OnePassSignatures); - - automaton.next(InputAlphabet.CompressedData); - automaton.next(InputAlphabet.LiteralData); - automaton.next(InputAlphabet.EndOfSequence); - - automaton.next(InputAlphabet.Signatures); - automaton.next(InputAlphabet.EndOfSequence); - - automaton.next(InputAlphabet.Signatures); - automaton.next(InputAlphabet.EndOfSequence); - - assertTrue(automaton.isValid()); - } -} From 61949240b372abde7da5e29b40207d825610ba24 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 19 Sep 2022 13:07:33 +0200 Subject: [PATCH 09/80] WIP: Add LayerMetadata class --- .../pgpainless/algorithm/OpenPgpPacket.java | 2 +- .../OpenPgpMessageInputStream.java | 271 ++++++++++-------- 2 files changed, 149 insertions(+), 124 deletions(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/algorithm/OpenPgpPacket.java b/pgpainless-core/src/main/java/org/pgpainless/algorithm/OpenPgpPacket.java index 63d14a31..41e3fb08 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/algorithm/OpenPgpPacket.java +++ b/pgpainless-core/src/main/java/org/pgpainless/algorithm/OpenPgpPacket.java @@ -29,7 +29,7 @@ public enum OpenPgpPacket { PSK(PacketTags.PUBLIC_SUBKEY), UATTR(PacketTags.USER_ATTRIBUTE), SEIPD(PacketTags.SYM_ENC_INTEGRITY_PRO), - MOD(PacketTags.MOD_DETECTION_CODE), + MDC(PacketTags.MOD_DETECTION_CODE), EXP_1(PacketTags.EXPERIMENTAL_1), EXP_2(PacketTags.EXPERIMENTAL_2), 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 8dff7189..1a38fced 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 @@ -1,3 +1,7 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + package org.pgpainless.decryption_verification; import org.bouncycastle.bcpg.BCPGInputStream; @@ -27,9 +31,12 @@ 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.SymmetricKeyAlgorithm; import org.pgpainless.decryption_verification.automaton.InputAlphabet; import org.pgpainless.decryption_verification.automaton.PDA; import org.pgpainless.decryption_verification.automaton.StackAlphabet; @@ -55,15 +62,22 @@ public class OpenPgpMessageInputStream extends InputStream { protected final PDA automaton = new PDA(); protected final ConsumerOptions options; + protected final OpenPgpMetadata.Builder resultBuilder; protected final BCPGInputStream bcpgIn; protected InputStream in; private boolean closed = false; private Signatures signatures; + private LayerMetadata layerMetadata; public OpenPgpMessageInputStream(InputStream inputStream, ConsumerOptions options) throws IOException, PGPException { + this(inputStream, options, null); + } + + OpenPgpMessageInputStream(InputStream inputStream, ConsumerOptions options, LayerMetadata layerMetadata) + throws PGPException, IOException { // TODO: Use BCPGInputStream.wrap(inputStream); if (inputStream instanceof BCPGInputStream) { this.bcpgIn = (BCPGInputStream) inputStream; @@ -72,12 +86,35 @@ public class OpenPgpMessageInputStream extends InputStream { } this.options = options; + 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; + } + } + /** * This method consumes OpenPGP packets from the current {@link BCPGInputStream}. * Once it reaches a "nested" OpenPGP packet (Literal Data, Compressed Data, Encrypted Data), it sets

in
@@ -92,7 +129,7 @@ public class OpenPgpMessageInputStream extends InputStream { throws IOException, PGPException { System.out.println("Walk " + automaton); int tag; - loop: while ((tag = getTag()) != -1) { + loop: while ((tag = nextTag()) != -1) { OpenPgpPacket nextPacket = OpenPgpPacket.requireFromTag(tag); System.out.println(nextPacket); switch (nextPacket) { @@ -108,7 +145,9 @@ public class OpenPgpMessageInputStream extends InputStream { case COMP: automaton.next(InputAlphabet.CompressedData); PGPCompressedData compressedData = new PGPCompressedData(bcpgIn); - in = new OpenPgpMessageInputStream(compressedData.getDataStream(), options); + LayerMetadata compressionLayer = new LayerMetadata(); + compressionLayer.setCompressionAlgorithm(CompressionAlgorithm.fromId(compressedData.getAlgorithm())); + in = new OpenPgpMessageInputStream(compressedData.getDataStream(), options, compressionLayer); break loop; // One Pass Signatures @@ -119,10 +158,10 @@ public class OpenPgpMessageInputStream extends InputStream { // Signatures - either prepended to the message, or corresponding to the One Pass Signatures case SIG: - boolean isCorrespondingToOPS = automaton.peekStack() == StackAlphabet.ops; + boolean isSigForOPS = automaton.peekStack() == StackAlphabet.ops; automaton.next(InputAlphabet.Signatures); PGPSignatureList signatureList = readSignatures(); - if (isCorrespondingToOPS) { + if (isSigForOPS) { signatures.addOnePassCorrespondingSignatures(signatureList); } else { signatures.addPrependedSignatures(signatureList); @@ -135,99 +174,15 @@ public class OpenPgpMessageInputStream extends InputStream { case SED: case SEIPD: automaton.next(InputAlphabet.EncryptedData); - PGPEncryptedDataList encDataList = new PGPEncryptedDataList(bcpgIn); - - // TODO: Replace with !encDataList.isIntegrityProtected() - if (!encDataList.get(0).isIntegrityProtected()) { - throw new MessageNotIntegrityProtectedException(); + if (processEncryptedData()) { + break loop; } - SortedESKs esks = new SortedESKs(encDataList); - - if (options.getSessionKey() != null) { - SessionKeyDataDecryptorFactory decryptorFactory = ImplementationFactory.getInstance() - .getSessionKeyDataDecryptorFactory(options.getSessionKey()); - // TODO: Replace with encDataList.addSessionKeyDecryptionMethod(sessionKey) - PGPEncryptedData esk = esks.all().get(0); - try { - if (esk instanceof PGPPBEEncryptedData) { - PGPPBEEncryptedData skesk = (PGPPBEEncryptedData) esk; - in = skesk.getDataStream(decryptorFactory); - break loop; - } else if (esk instanceof PGPPublicKeyEncryptedData) { - PGPPublicKeyEncryptedData pkesk = (PGPPublicKeyEncryptedData) esk; - in = pkesk.getDataStream(decryptorFactory); - break loop; - } else { - throw new RuntimeException("Unknown ESK class type: " + esk.getClass().getName()); - } - } catch (PGPException e) { - // Session key mismatch? - } - } - - // Try passwords - for (PGPPBEEncryptedData skesk : esks.skesks) { - for (Passphrase passphrase : options.getDecryptionPassphrases()) { - PBEDataDecryptorFactory decryptorFactory = ImplementationFactory.getInstance() - .getPBEDataDecryptorFactory(passphrase); - try { - InputStream decrypted = skesk.getDataStream(decryptorFactory); - in = new OpenPgpMessageInputStream(decrypted, options); - break loop; - } catch (PGPException e) { - // password mismatch? Try next password - } - - } - } - - // Try (known) secret keys - for (PGPPublicKeyEncryptedData pkesk : esks.pkesks) { - long keyId = pkesk.getKeyID(); - PGPSecretKeyRing decryptionKeys = getDecryptionKey(keyId); - if (decryptionKeys == null) { - continue; - } - SecretKeyRingProtector protector = options.getSecretKeyProtector(decryptionKeys); - PGPSecretKey decryptionKey = decryptionKeys.getSecretKey(keyId); - PGPPrivateKey privateKey = UnlockSecretKey.unlockSecretKey(decryptionKey, protector); - - PublicKeyDataDecryptorFactory decryptorFactory = ImplementationFactory.getInstance() - .getPublicKeyDataDecryptorFactory(privateKey); - try { - InputStream decrypted = pkesk.getDataStream(decryptorFactory); - in = new OpenPgpMessageInputStream(decrypted, options); - break loop; - } catch (PGPException e) { - // hm :/ - } - } - - // try anonymous secret keys - for (PGPPublicKeyEncryptedData pkesk : esks.anonPkesks) { - for (Tuple decryptionKeyCandidate : findPotentialDecryptionKeys(pkesk)) { - SecretKeyRingProtector protector = options.getSecretKeyProtector(decryptionKeyCandidate.getA()); - PGPPrivateKey privateKey = UnlockSecretKey.unlockSecretKey(decryptionKeyCandidate.getB(), protector); - PublicKeyDataDecryptorFactory decryptorFactory = ImplementationFactory.getInstance() - .getPublicKeyDataDecryptorFactory(privateKey); - - try { - InputStream decrypted = pkesk.getDataStream(decryptorFactory); - in = new OpenPgpMessageInputStream(decrypted, options); - break loop; - } catch (PGPException e) { - // hm :/ - } - } - } - - // TODO: try interactive password callbacks - throw new MissingDecryptionMethodException("No working decryption method found."); + // Marker Packets need to be skipped and ignored case MARKER: - bcpgIn.readPacket(); // skip marker packet + bcpgIn.readPacket(); // skip break; // Key Packets are illegal in this context @@ -238,8 +193,11 @@ public class OpenPgpMessageInputStream extends InputStream { case TRUST: case UID: case UATTR: + throw new MalformedOpenPgpMessageException("Illegal Packet in Stream: " + nextPacket); - case MOD: + // MDC packet is usually processed by PGPEncryptedDataList, so it is very likely we encounter this + // packet out of order + case MDC: throw new MalformedOpenPgpMessageException("Unexpected Packet in Stream: " + nextPacket); // Experimental Packets are not supported @@ -252,7 +210,100 @@ public class OpenPgpMessageInputStream extends InputStream { } } - private int getTag() throws IOException { + private boolean processEncryptedData() throws IOException, PGPException { + PGPEncryptedDataList encDataList = new PGPEncryptedDataList(bcpgIn); + + // TODO: Replace with !encDataList.isIntegrityProtected() + if (!encDataList.get(0).isIntegrityProtected()) { + throw new MessageNotIntegrityProtectedException(); + } + + SortedESKs esks = new SortedESKs(encDataList); + + // Try session key + if (options.getSessionKey() != null) { + SessionKeyDataDecryptorFactory decryptorFactory = ImplementationFactory.getInstance() + .getSessionKeyDataDecryptorFactory(options.getSessionKey()); + // TODO: Replace with encDataList.addSessionKeyDecryptionMethod(sessionKey) + PGPEncryptedData esk = esks.all().get(0); + try { + if (esk instanceof PGPPBEEncryptedData) { + PGPPBEEncryptedData skesk = (PGPPBEEncryptedData) esk; + in = skesk.getDataStream(decryptorFactory); + return true; + } else if (esk instanceof PGPPublicKeyEncryptedData) { + PGPPublicKeyEncryptedData pkesk = (PGPPublicKeyEncryptedData) esk; + in = pkesk.getDataStream(decryptorFactory); + return true; + } else { + throw new RuntimeException("Unknown ESK class type: " + esk.getClass().getName()); + } + } catch (PGPException e) { + // Session key mismatch? + } + } + + // Try passwords + for (PGPPBEEncryptedData skesk : esks.skesks) { + for (Passphrase passphrase : options.getDecryptionPassphrases()) { + PBEDataDecryptorFactory decryptorFactory = ImplementationFactory.getInstance() + .getPBEDataDecryptorFactory(passphrase); + try { + InputStream decrypted = skesk.getDataStream(decryptorFactory); + in = new OpenPgpMessageInputStream(decrypted, options); + return true; + } catch (PGPException e) { + // password mismatch? Try next password + } + + } + } + + // Try (known) secret keys + for (PGPPublicKeyEncryptedData pkesk : esks.pkesks) { + long keyId = pkesk.getKeyID(); + PGPSecretKeyRing decryptionKeys = getDecryptionKey(keyId); + if (decryptionKeys == null) { + continue; + } + SecretKeyRingProtector protector = options.getSecretKeyProtector(decryptionKeys); + PGPSecretKey decryptionKey = decryptionKeys.getSecretKey(keyId); + PGPPrivateKey privateKey = UnlockSecretKey.unlockSecretKey(decryptionKey, protector); + + PublicKeyDataDecryptorFactory decryptorFactory = ImplementationFactory.getInstance() + .getPublicKeyDataDecryptorFactory(privateKey); + try { + InputStream decrypted = pkesk.getDataStream(decryptorFactory); + in = new OpenPgpMessageInputStream(decrypted, options); + return true; + } catch (PGPException e) { + // hm :/ + } + } + + // try anonymous secret keys + for (PGPPublicKeyEncryptedData pkesk : esks.anonPkesks) { + for (Tuple decryptionKeyCandidate : findPotentialDecryptionKeys(pkesk)) { + SecretKeyRingProtector protector = options.getSecretKeyProtector(decryptionKeyCandidate.getA()); + PGPPrivateKey privateKey = UnlockSecretKey.unlockSecretKey(decryptionKeyCandidate.getB(), protector); + PublicKeyDataDecryptorFactory decryptorFactory = ImplementationFactory.getInstance() + .getPublicKeyDataDecryptorFactory(privateKey); + + try { + InputStream decrypted = pkesk.getDataStream(decryptorFactory); + in = new OpenPgpMessageInputStream(decrypted, options); + return true; + } catch (PGPException e) { + // hm :/ + } + } + } + + // we did not yet succeed in decrypting any session key :/ + return false; + } + + private int nextTag() throws IOException { try { return bcpgIn.nextPacketTag(); } catch (IOException e) { @@ -296,7 +347,7 @@ public class OpenPgpMessageInputStream extends InputStream { ByteArrayOutputStream buf = new ByteArrayOutputStream(); BCPGOutputStream bcpgOut = new BCPGOutputStream(buf); int tag; - while ((tag = getTag()) == PacketTags.ONE_PASS_SIGNATURE || tag == PacketTags.MARKER) { + while ((tag = nextTag()) == PacketTags.ONE_PASS_SIGNATURE || tag == PacketTags.MARKER) { Packet packet = bcpgIn.readPacket(); if (tag == PacketTags.ONE_PASS_SIGNATURE) { OnePassSignaturePacket sigPacket = (OnePassSignaturePacket) packet; @@ -313,13 +364,13 @@ public class OpenPgpMessageInputStream extends InputStream { private PGPSignatureList readSignatures() throws IOException { ByteArrayOutputStream buf = new ByteArrayOutputStream(); BCPGOutputStream bcpgOut = new BCPGOutputStream(buf); - int tag = getTag(); + int tag = nextTag(); while (tag == PacketTags.SIGNATURE || tag == PacketTags.MARKER) { Packet packet = bcpgIn.readPacket(); if (tag == PacketTags.SIGNATURE) { SignaturePacket sigPacket = (SignaturePacket) packet; sigPacket.encode(bcpgOut); - tag = getTag(); + tag = nextTag(); } } bcpgOut.close(); @@ -356,20 +407,6 @@ public class OpenPgpMessageInputStream extends InputStream { throw new RuntimeException(e); } signatures.finish(); - /* - if (in instanceof OpenPgpMessageInputStream) { - in.close(); - in = null; - } else { - try { - System.out.println("Read consume"); - consumePackets(); - signatures.finish(); - } catch (PGPException e) { - throw new RuntimeException(e); - } - } - */ } return r; } @@ -394,18 +431,6 @@ public class OpenPgpMessageInputStream extends InputStream { throw new RuntimeException(e); } signatures.finish(); - /* - if (in instanceof OpenPgpMessageInputStream) { - in.close(); - in = null; - } else { - try { - consumePackets(); - } catch (PGPException e) { - throw new RuntimeException(e); - } - } - */ } return r; } From a3957d33724d0d30311082c5565ac970542563cb Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Fri, 23 Sep 2022 14:04:44 +0200 Subject: [PATCH 10/80] WIP: Play around with TeeInputStreams --- .../OpenPgpMessageInputStream.java | 33 +++++++++-- .../TeeBCPGInputStream.java | 35 +++++++++++ .../TeeBCPGInputStreamTest.java | 58 +++++++++++++++++++ 3 files changed, 121 insertions(+), 5 deletions(-) create mode 100644 pgpainless-core/src/main/java/org/pgpainless/decryption_verification/TeeBCPGInputStream.java create mode 100644 pgpainless-core/src/test/java/org/pgpainless/decryption_verification/TeeBCPGInputStreamTest.java 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 1a38fced..990e628d 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 @@ -343,7 +343,8 @@ public class OpenPgpMessageInputStream extends InputStream { return null; } - private PGPOnePassSignatureList readOnePassSignatures() throws IOException { + private PGPOnePassSignatureListWrapper readOnePassSignatures() throws IOException { + List encapsulating = new ArrayList<>(); ByteArrayOutputStream buf = new ByteArrayOutputStream(); BCPGOutputStream bcpgOut = new BCPGOutputStream(buf); int tag; @@ -351,14 +352,16 @@ public class OpenPgpMessageInputStream extends InputStream { Packet packet = bcpgIn.readPacket(); if (tag == PacketTags.ONE_PASS_SIGNATURE) { OnePassSignaturePacket sigPacket = (OnePassSignaturePacket) packet; - sigPacket.encode(bcpgOut); + byte[] bytes = sigPacket.getEncoded(); + encapsulating.add(bytes[bytes.length - 1] == 1); + bcpgOut.write(bytes); } } bcpgOut.close(); PGPObjectFactory objectFactory = ImplementationFactory.getInstance().getPGPObjectFactory(buf.toByteArray()); PGPOnePassSignatureList signatureList = (PGPOnePassSignatureList) objectFactory.nextObject(); - return signatureList; + return new PGPOnePassSignatureListWrapper(signatureList, encapsulating); } private PGPSignatureList readSignatures() throws IOException { @@ -490,6 +493,26 @@ public class OpenPgpMessageInputStream extends InputStream { } } + /** + * Workaround for BC not exposing, whether an OPS is encapsulating or not. + * TODO: Remove once our PR is merged + * + * @see PR against BC + */ + private static class PGPOnePassSignatureListWrapper { + private final PGPOnePassSignatureList list; + private final List encapsulating; + + public PGPOnePassSignatureListWrapper(PGPOnePassSignatureList signatures, List encapsulating) { + this.list = signatures; + this.encapsulating = encapsulating; + } + + public int size() { + return list.size(); + } + } + private static class Signatures { final ConsumerOptions options; List detachedSignatures = new ArrayList<>(); @@ -521,9 +544,9 @@ public class OpenPgpMessageInputStream extends InputStream { } } - void addOnePassSignatures(PGPOnePassSignatureList signatures) { + void addOnePassSignatures(PGPOnePassSignatureListWrapper signatures) { System.out.println("Adding " + signatures.size() + " OPSs"); - for (PGPOnePassSignature ops : signatures) { + for (PGPOnePassSignature ops : signatures.list) { PGPPublicKeyRing certificate = findCertificate(ops.getKeyID()); initialize(ops, certificate); this.onePassSignatures.add(ops); diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/TeeBCPGInputStream.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/TeeBCPGInputStream.java new file mode 100644 index 00000000..ab24f22e --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/TeeBCPGInputStream.java @@ -0,0 +1,35 @@ +package org.pgpainless.decryption_verification; + +import org.bouncycastle.bcpg.BCPGInputStream; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +public class TeeBCPGInputStream extends BCPGInputStream { + + private final OutputStream out; + + public TeeBCPGInputStream(InputStream in, OutputStream outputStream) { + super(in); + this.out = outputStream; + } + + @Override + public int read() throws IOException { + int r = super.read(); + if (r != -1) { + out.write(r); + } + return r; + } + + @Override + public int read(byte[] buf, int off, int len) throws IOException { + int r = super.read(buf, off, len); + if (r > 0) { + out.write(buf, off, r); + } + return r; + } +} diff --git a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/TeeBCPGInputStreamTest.java b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/TeeBCPGInputStreamTest.java new file mode 100644 index 00000000..765221d3 --- /dev/null +++ b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/TeeBCPGInputStreamTest.java @@ -0,0 +1,58 @@ +package org.pgpainless.decryption_verification; + +import org.bouncycastle.bcpg.ArmoredInputStream; +import org.bouncycastle.bcpg.ArmoredOutputStream; +import org.bouncycastle.bcpg.BCPGInputStream; +import org.bouncycastle.bcpg.Packet; +import org.bouncycastle.openpgp.PGPCompressedData; +import org.bouncycastle.openpgp.PGPException; +import org.junit.jupiter.api.Test; +import org.pgpainless.algorithm.OpenPgpPacket; +import org.pgpainless.util.ArmoredInputStreamFactory; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; + +public class TeeBCPGInputStreamTest { + + private static final String INBAND_SIGNED = "-----BEGIN PGP MESSAGE-----\n" + + "Version: PGPainless\n" + + "\n" + + "owGbwMvMyCUWdXSHvVTUtXbG0yJJDCDgkZqTk6+jEJ5flJOiyNVRysIoxsXAxsqU\n" + + "GDiVjUGRUwCmQUyRRWnOn9Z/PIseF3Yz6cCEL05nZDj1OClo75WVTjNmJPemW6qV\n" + + "6ki//1K1++2s0qTP+0N11O4z/BVLDDdxnmQryS+5VXjBX7/0Hxnm/eqeX6Zum35r\n" + + "M8e7ufwA\n" + + "=RDiy\n" + + "-----END PGP MESSAGE-----"; + + @Test + public void test() throws IOException, PGPException { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + ArmoredOutputStream armorOut = new ArmoredOutputStream(out); + + ByteArrayInputStream bytesIn = new ByteArrayInputStream(INBAND_SIGNED.getBytes(StandardCharsets.UTF_8)); + ArmoredInputStream armorIn = ArmoredInputStreamFactory.get(bytesIn); + BCPGInputStream bcpgIn = new BCPGInputStream(armorIn); + TeeBCPGInputStream teeIn = new TeeBCPGInputStream(bcpgIn, armorOut); + + ByteArrayOutputStream nestedOut = new ByteArrayOutputStream(); + ArmoredOutputStream nestedArmorOut = new ArmoredOutputStream(nestedOut); + + PGPCompressedData compressedData = new PGPCompressedData(teeIn); + InputStream nestedStream = compressedData.getDataStream(); + BCPGInputStream nestedBcpgIn = new BCPGInputStream(nestedStream); + TeeBCPGInputStream nestedTeeIn = new TeeBCPGInputStream(nestedBcpgIn, nestedArmorOut); + + int tag; + while ((tag = nestedTeeIn.nextPacketTag()) != -1) { + System.out.println(OpenPgpPacket.requireFromTag(tag)); + Packet packet = nestedTeeIn.readPacket(); + } + + nestedArmorOut.close(); + System.out.println(nestedOut); + } +} From 714e424eacb7711a3c544ac3d1bf52887ddb4bbe Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 26 Sep 2022 18:21:06 +0200 Subject: [PATCH 11/80] 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); From 7cb22f1530fc43cb244ea7e0dc5f615fad0d6e37 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 27 Sep 2022 16:11:55 +0200 Subject: [PATCH 12/80] Fix checkstyle issues --- .../MessageMetadata.java | 6 +- .../OpenPgpMessageInputStream.java | 23 +++---- .../automaton/InputAlphabet.java | 4 ++ .../automaton/PDA.java | 4 ++ .../automaton/StackAlphabet.java | 4 ++ .../automaton/package-info.java | 8 +++ .../OpenPgpMessageInputStreamTest.java | 60 +++++++++++-------- .../TeeBCPGInputStreamTest.java | 7 ++- 8 files changed, 75 insertions(+), 41 deletions(-) create mode 100644 pgpainless-core/src/main/java/org/pgpainless/decryption_verification/automaton/package-info.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 index 7c09fc8d..cbf251a8 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/MessageMetadata.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/MessageMetadata.java @@ -90,7 +90,7 @@ public class MessageMetadata { return (LiteralData) nested; } - public static abstract class Layer { + public abstract static class Layer { protected final List verifiedSignatures = new ArrayList<>(); protected final List failedSignatures = new ArrayList<>(); protected Nested child; @@ -198,11 +198,11 @@ public class MessageMetadata { } - private static abstract class LayerIterator implements Iterator { + private abstract static class LayerIterator implements Iterator { private Nested current; Layer last = null; - public LayerIterator(Message message) { + LayerIterator(Message message) { super(); this.current = message.child; if (matches(current)) { 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 44ebb27e..58699fcd 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 @@ -50,6 +50,8 @@ import org.pgpainless.key.protection.UnlockSecretKey; import org.pgpainless.signature.SignatureUtils; import org.pgpainless.util.Passphrase; import org.pgpainless.util.Tuple; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import java.io.ByteArrayOutputStream; import java.io.IOException; @@ -60,6 +62,8 @@ import java.util.List; public class OpenPgpMessageInputStream extends InputStream { + private static final Logger LOGGER = LoggerFactory.getLogger(OpenPgpMessageInputStream.class); + protected final PDA automaton = new PDA(); protected final ConsumerOptions options; protected final OpenPgpMetadata.Builder resultBuilder; @@ -519,7 +523,7 @@ public class OpenPgpMessageInputStream extends InputStream { private final PGPOnePassSignatureList list; private final List encapsulating; - public PGPOnePassSignatureListWrapper(PGPOnePassSignatureList signatures, List encapsulating) { + PGPOnePassSignatureListWrapper(PGPOnePassSignatureList signatures, List encapsulating) { this.list = signatures; this.encapsulating = encapsulating; } @@ -529,7 +533,7 @@ public class OpenPgpMessageInputStream extends InputStream { } } - private static class Signatures { + private static final class Signatures { final ConsumerOptions options; List detachedSignatures = new ArrayList<>(); List prependedSignatures = new ArrayList<>(); @@ -551,7 +555,6 @@ public class OpenPgpMessageInputStream extends InputStream { } void addPrependedSignatures(PGPSignatureList signatures) { - System.out.println("Adding " + signatures.size() + " prepended Signatures"); for (PGPSignature signature : signatures) { long keyId = SignatureUtils.determineIssuerKeyId(signature); PGPPublicKeyRing certificate = findCertificate(keyId); @@ -561,7 +564,6 @@ public class OpenPgpMessageInputStream extends InputStream { } void addOnePassSignatures(PGPOnePassSignatureListWrapper signatures) { - System.out.println("Adding " + signatures.size() + " OPSs"); for (PGPOnePassSignature ops : signatures.list) { PGPPublicKeyRing certificate = findCertificate(ops.getKeyID()); initialize(ops, certificate); @@ -570,7 +572,6 @@ public class OpenPgpMessageInputStream extends InputStream { } void addOnePassCorrespondingSignatures(PGPSignatureList signatures) { - System.out.println("Adding " + signatures.size() + " Corresponding Signatures"); for (PGPSignature signature : signatures) { correspondingSignatures.add(signature); } @@ -631,9 +632,9 @@ public class OpenPgpMessageInputStream extends InputStream { try { verified = detached.verify(); } catch (PGPException e) { - System.out.println(e.getMessage()); + LOGGER.debug("Cannot verify detached signature.", e); } - System.out.println("Detached Signature by " + Long.toHexString(detached.getKeyID()) + " is " + (verified ? "verified" : "unverified")); + LOGGER.debug("Detached Signature by " + Long.toHexString(detached.getKeyID()) + " is " + (verified ? "verified" : "unverified")); } for (PGPSignature prepended : prependedSignatures) { @@ -641,9 +642,9 @@ public class OpenPgpMessageInputStream extends InputStream { try { verified = prepended.verify(); } catch (PGPException e) { - System.out.println(e.getMessage()); + LOGGER.debug("Cannot verify prepended signature.", e); } - System.out.println("Prepended Signature by " + Long.toHexString(prepended.getKeyID()) + " is " + (verified ? "verified" : "unverified")); + LOGGER.debug("Prepended Signature by " + Long.toHexString(prepended.getKeyID()) + " is " + (verified ? "verified" : "unverified")); } @@ -654,9 +655,9 @@ public class OpenPgpMessageInputStream extends InputStream { try { verified = ops.verify(signature); } catch (PGPException e) { - System.out.println(e.getMessage()); + LOGGER.debug("Cannot verify OPS signature.", e); } - System.out.println("One-Pass-Signature by " + Long.toHexString(ops.getKeyID()) + " is " + (verified ? "verified" : "unverified")); + LOGGER.debug("One-Pass-Signature by " + Long.toHexString(ops.getKeyID()) + " is " + (verified ? "verified" : "unverified")); } } } diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/automaton/InputAlphabet.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/automaton/InputAlphabet.java index d015a4b3..8e795f5b 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/automaton/InputAlphabet.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/automaton/InputAlphabet.java @@ -1,3 +1,7 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + package org.pgpainless.decryption_verification.automaton; import org.bouncycastle.openpgp.PGPCompressedData; 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 feb759ea..dda2adce 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 @@ -1,3 +1,7 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + package org.pgpainless.decryption_verification.automaton; import org.pgpainless.exception.MalformedOpenPgpMessageException; diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/automaton/StackAlphabet.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/automaton/StackAlphabet.java index 97dad3d8..09865f31 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/automaton/StackAlphabet.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/automaton/StackAlphabet.java @@ -1,3 +1,7 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + package org.pgpainless.decryption_verification.automaton; public enum StackAlphabet { diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/automaton/package-info.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/automaton/package-info.java new file mode 100644 index 00000000..80a79e85 --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/automaton/package-info.java @@ -0,0 +1,8 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +/** + * Pushdown Automaton to verify validity of packet sequences according to the OpenPGP Message format. + */ +package org.pgpainless.decryption_verification.automaton; 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 8219ec68..6962e279 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 @@ -35,6 +35,7 @@ import org.junit.jupiter.params.provider.MethodSource; import org.pgpainless.PGPainless; import org.pgpainless.algorithm.CompressionAlgorithm; import org.pgpainless.algorithm.StreamEncoding; +import org.pgpainless.algorithm.SymmetricKeyAlgorithm; import org.pgpainless.encryption_signing.EncryptionOptions; import org.pgpainless.encryption_signing.EncryptionResult; import org.pgpainless.encryption_signing.EncryptionStream; @@ -118,7 +119,7 @@ public class OpenPgpMessageInputStreamTest { "=K9Zl\n" + "-----END PGP MESSAGE-----"; - public static final String SIG_LIT = "" + + public static final String SIG_COMP_LIT = "" + "-----BEGIN PGP MESSAGE-----\n" + "Version: BCPG v1.71\n" + "\n" + @@ -235,9 +236,9 @@ public class OpenPgpMessageInputStreamTest { } public static void genKey() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, IOException { - System.out.println(PGPainless.asciiArmor( - PGPainless.generateKeyRing().modernKeyRing("Alice ") - )); + PGPainless.asciiArmor( + PGPainless.generateKeyRing().modernKeyRing("Alice "), + System.out); } public static void genSIG_LIT() throws PGPException, IOException { @@ -265,54 +266,46 @@ public class OpenPgpMessageInputStreamTest { armorOut.close(); String armored = out.toString(); + // CHECKSTYLE:OFF System.out.println(armored .replace("-----BEGIN PGP SIGNATURE-----\n", "-----BEGIN PGP MESSAGE-----\n") .replace("-----END PGP SIGNATURE-----", "-----END PGP MESSAGE-----")); + // CHECKSTYLE:ON } public static void genSENC_LIT() throws PGPException, IOException { - ByteArrayOutputStream out = new ByteArrayOutputStream(); EncryptionStream enc = PGPainless.encryptAndOrSign() - .onOutputStream(out) + .onOutputStream(System.out) .withOptions(ProducerOptions.encrypt(EncryptionOptions.get() .addPassphrase(Passphrase.fromPassword(PASSPHRASE))) .overrideCompressionAlgorithm(CompressionAlgorithm.UNCOMPRESSED)); enc.write(PLAINTEXT.getBytes(StandardCharsets.UTF_8)); enc.close(); - - System.out.println(out); } public static void genPENC_COMP_LIT() throws IOException, PGPException { PGPSecretKeyRing secretKeys = PGPainless.readKeyRing().secretKeyRing(KEY); PGPPublicKeyRing cert = PGPainless.extractCertificate(secretKeys); - - ByteArrayOutputStream out = new ByteArrayOutputStream(); EncryptionStream enc = PGPainless.encryptAndOrSign() - .onOutputStream(out) + .onOutputStream(System.out) .withOptions(ProducerOptions.encrypt(EncryptionOptions.get() .addRecipient(cert)) .overrideCompressionAlgorithm(CompressionAlgorithm.ZLIB)); Streams.pipeAll(new ByteArrayInputStream(PLAINTEXT.getBytes(StandardCharsets.UTF_8)), enc); enc.close(); - - System.out.println(out); } public static void genOPS_LIT_SIG() throws PGPException, IOException { PGPSecretKeyRing secretKeys = PGPainless.readKeyRing().secretKeyRing(KEY); - ByteArrayOutputStream out = new ByteArrayOutputStream(); EncryptionStream enc = PGPainless.encryptAndOrSign() - .onOutputStream(out) + .onOutputStream(System.out) .withOptions(ProducerOptions.sign(SigningOptions.get() .addSignature(SecretKeyRingProtector.unprotectedKeys(), secretKeys)) .overrideCompressionAlgorithm(CompressionAlgorithm.UNCOMPRESSED)); Streams.pipeAll(new ByteArrayInputStream(PLAINTEXT.getBytes(StandardCharsets.UTF_8)), enc); enc.close(); - - System.out.println(out); } interface Processor { @@ -322,7 +315,8 @@ public class OpenPgpMessageInputStreamTest { 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))); + Arguments.of(Named.of("read()", (Processor) OpenPgpMessageInputStreamTest::processReadSequential)) + ); } @ParameterizedTest(name = "Process LIT using {0}") @@ -349,7 +343,8 @@ public class OpenPgpMessageInputStreamTest { @ParameterizedTest(name = "Process COMP(LIT) using {0}") @MethodSource("provideMessageProcessors") - public void testProcessCOMP_LIT(Processor processor) throws PGPException, IOException { + public void testProcessCOMP_LIT(Processor processor) + throws PGPException, IOException { Tuple result = processor.process(COMP_LIT, ConsumerOptions.get()); String plain = result.getA(); assertEquals(PLAINTEXT, plain); @@ -366,7 +361,8 @@ public class OpenPgpMessageInputStreamTest { @ParameterizedTest(name = "Process COMP(COMP(LIT)) using {0}") @MethodSource("provideMessageProcessors") - public void testProcessCOMP_COMP_LIT(Processor processor) throws PGPException, IOException { + 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); @@ -376,29 +372,37 @@ public class OpenPgpMessageInputStreamTest { assertEquals(CompressionAlgorithm.ZIP, compressionAlgorithms.next()); assertEquals(CompressionAlgorithm.BZIP2, compressionAlgorithms.next()); assertFalse(compressionAlgorithms.hasNext()); + assertNull(metadata.getEncryptionAlgorithm()); } - @ParameterizedTest(name = "Process SIG LIT using {0}") + @ParameterizedTest(name = "Process SIG COMP(LIT) using {0}") @MethodSource("provideMessageProcessors") - public void testProcessSIG_LIT(Processor processor) throws PGPException, IOException { + public void testProcessSIG_COMP_LIT(Processor processor) throws PGPException, IOException { PGPPublicKeyRing cert = PGPainless.extractCertificate( PGPainless.readKeyRing().secretKeyRing(KEY)); - Tuple result = processor.process(SIG_LIT, ConsumerOptions.get() + Tuple result = processor.process(SIG_COMP_LIT, ConsumerOptions.get() .addVerificationCert(cert)); String plain = result.getA(); assertEquals(PLAINTEXT, plain); + MessageMetadata metadata = result.getB(); + assertEquals(CompressionAlgorithm.ZIP, metadata.getCompressionAlgorithm()); + assertNull(metadata.getEncryptionAlgorithm()); } @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))); + Tuple result = processor.process(SENC_LIT, ConsumerOptions.get() + .addDecryptionPassphrase(Passphrase.fromPassword(PASSPHRASE))); String plain = result.getA(); assertEquals(PLAINTEXT, plain); + MessageMetadata metadata = result.getB(); + assertNull(metadata.getCompressionAlgorithm()); + assertEquals(SymmetricKeyAlgorithm.AES_256, metadata.getEncryptionAlgorithm()); } - @ParameterizedTest(name = "Process PENC(LIT) using {0}") + @ParameterizedTest(name = "Process PENC(COMP(LIT)) using {0}") @MethodSource("provideMessageProcessors") public void testProcessPENC_COMP_LIT(Processor processor) throws IOException, PGPException { PGPSecretKeyRing secretKeys = PGPainless.readKeyRing().secretKeyRing(KEY); @@ -406,6 +410,9 @@ public class OpenPgpMessageInputStreamTest { .addDecryptionKey(secretKeys)); String plain = result.getA(); assertEquals(PLAINTEXT, plain); + MessageMetadata metadata = result.getB(); + assertEquals(CompressionAlgorithm.ZLIB, metadata.getCompressionAlgorithm()); + assertEquals(SymmetricKeyAlgorithm.AES_256, metadata.getEncryptionAlgorithm()); } @ParameterizedTest(name = "Process OPS LIT SIG using {0}") @@ -416,6 +423,9 @@ public class OpenPgpMessageInputStreamTest { .addVerificationCert(cert)); String plain = result.getA(); assertEquals(PLAINTEXT, plain); + MessageMetadata metadata = result.getB(); + assertNull(metadata.getEncryptionAlgorithm()); + assertNull(metadata.getCompressionAlgorithm()); } private static Tuple processReadBuffered(String armoredMessage, ConsumerOptions options) throws PGPException, IOException { diff --git a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/TeeBCPGInputStreamTest.java b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/TeeBCPGInputStreamTest.java index 765221d3..d31c6009 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/TeeBCPGInputStreamTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/TeeBCPGInputStreamTest.java @@ -9,6 +9,8 @@ import org.bouncycastle.openpgp.PGPException; import org.junit.jupiter.api.Test; import org.pgpainless.algorithm.OpenPgpPacket; import org.pgpainless.util.ArmoredInputStreamFactory; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; @@ -18,6 +20,7 @@ import java.nio.charset.StandardCharsets; public class TeeBCPGInputStreamTest { + private static final Logger LOGGER = LoggerFactory.getLogger(TeeBCPGInputStreamTest.class); private static final String INBAND_SIGNED = "-----BEGIN PGP MESSAGE-----\n" + "Version: PGPainless\n" + "\n" + @@ -48,11 +51,11 @@ public class TeeBCPGInputStreamTest { int tag; while ((tag = nestedTeeIn.nextPacketTag()) != -1) { - System.out.println(OpenPgpPacket.requireFromTag(tag)); + LOGGER.debug(OpenPgpPacket.requireFromTag(tag).toString()); Packet packet = nestedTeeIn.readPacket(); } nestedArmorOut.close(); - System.out.println(nestedOut); + LOGGER.debug(nestedOut.toString()); } } From f614c325cb4af4035b7885ba3c4a7304b2847cc4 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 28 Sep 2022 16:55:08 +0200 Subject: [PATCH 13/80] Wip: Work on OPS verification --- .../OpenPgpMessageInputStream.java | 200 ++++++++++++------ 1 file changed, 133 insertions(+), 67 deletions(-) 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 58699fcd..df3d6805 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 @@ -56,6 +56,7 @@ import org.slf4j.LoggerFactory; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; +import java.io.OutputStream; import java.util.ArrayList; import java.util.Collection; import java.util.List; @@ -64,15 +65,19 @@ public class OpenPgpMessageInputStream extends InputStream { private static final Logger LOGGER = LoggerFactory.getLogger(OpenPgpMessageInputStream.class); - protected final PDA automaton = new PDA(); + // Options to consume the data protected final ConsumerOptions options; protected final OpenPgpMetadata.Builder resultBuilder; - protected final BCPGInputStream bcpgIn; - protected InputStream in; + // Pushdown Automaton to verify validity of OpenPGP packet sequence in an OpenPGP message + protected final PDA automaton = new PDA(); + // InputStream of OpenPGP packets of the current layer + protected final BCPGInputStream packetInputStream; + // InputStream of a nested data packet + protected InputStream nestedInputStream; private boolean closed = false; - private Signatures signatures; + private final Signatures signatures; private MessageMetadata.Layer metadata; public OpenPgpMessageInputStream(InputStream inputStream, ConsumerOptions options) @@ -82,31 +87,45 @@ public class OpenPgpMessageInputStream extends InputStream { OpenPgpMessageInputStream(InputStream inputStream, ConsumerOptions options, MessageMetadata.Layer metadata) throws PGPException, IOException { - // TODO: Use BCPGInputStream.wrap(inputStream); - if (inputStream instanceof BCPGInputStream) { - this.bcpgIn = (BCPGInputStream) inputStream; - } else { - this.bcpgIn = new BCPGInputStream(inputStream); - } this.options = options; this.metadata = metadata; this.resultBuilder = OpenPgpMetadata.getBuilder(); this.signatures = new Signatures(options); - this.signatures.addDetachedSignatures(options.getDetachedSignatures()); - consumePackets(); // nom nom nom + // Add detached signatures only on the outermost OpenPgpMessageInputStream + if (metadata instanceof MessageMetadata.Message) { + this.signatures.addDetachedSignatures(options.getDetachedSignatures()); + } + + // TODO: Use BCPGInputStream.wrap(inputStream); + BCPGInputStream bcpg = null; + if (inputStream instanceof BCPGInputStream) { + bcpg = (BCPGInputStream) inputStream; + } else { + bcpg = new BCPGInputStream(inputStream); + } + this.packetInputStream = new TeeBCPGInputStream(bcpg, signatures); + + // *omnomnom* + consumePackets(); } /** - * This method consumes OpenPGP packets from the current {@link BCPGInputStream}. - * Once it reaches a "nested" OpenPGP packet (Literal Data, Compressed Data, Encrypted Data), it sets
in
- * to the nested stream and breaks the loop. + * Consume OpenPGP packets from the current {@link BCPGInputStream}. + * Once an OpenPGP packet with nested data (Literal Data, Compressed Data, Encrypted Data) is reached, + * set
nestedInputStream
to the nested stream and breaks the loop. * The nested stream is either a simple {@link InputStream} (in case of Literal Data), or another * {@link OpenPgpMessageInputStream} in case of Compressed and Encrypted Data. + * Once the nested data is processed, this method is called again to consume the remainder + * of packets following the nested data packet. * - * @throws IOException - * @throws PGPException + * @throws IOException in case of an IO error + * @throws PGPException in case of an OpenPGP error + * @throws MissingDecryptionMethodException if there is an encrypted data packet which cannot be decrypted + * due to missing decryption methods (no key, no password, no sessionkey) + * @throws MalformedOpenPgpMessageException if the message is made of an invalid packet sequence which + * does not follow the packet syntax of RFC4880. */ private void consumePackets() throws IOException, PGPException { @@ -127,17 +146,23 @@ public class OpenPgpMessageInputStream extends InputStream { processCompressedData(); break loop; - // One Pass Signatures + // One Pass Signature case OPS: automaton.next(InputAlphabet.OnePassSignatures); - signatures.addOnePassSignatures(readOnePassSignatures()); + signatures.addOnePassSignature(readOnePassSignature()); + // signatures.addOnePassSignatures(readOnePassSignatures()); break; - // Signatures - either prepended to the message, or corresponding to the One Pass Signatures + // Signature - either prepended to the message, or corresponding to a One Pass Signature case SIG: boolean isSigForOPS = automaton.peekStack() == StackAlphabet.ops; automaton.next(InputAlphabet.Signatures); - processSignature(isSigForOPS); + PGPSignature signature = readSignature(); + processSignature(signature, isSigForOPS); + /* + PGPSignatureList signatureList = readSignatures(); + processSignatures(signatureList, isSigForOPS); + */ break; // Encrypted Data (ESKs and SED/SEIPD are parsed the same by BC) @@ -154,7 +179,7 @@ public class OpenPgpMessageInputStream extends InputStream { // Marker Packets need to be skipped and ignored case MARKER: - bcpgIn.readPacket(); // skip + packetInputStream.readPacket(); // skip break; // Key Packets are illegal in this context @@ -182,8 +207,15 @@ public class OpenPgpMessageInputStream extends InputStream { } } - private void processSignature(boolean isSigForOPS) throws IOException { - PGPSignatureList signatureList = readSignatures(); + private void processSignature(PGPSignature signature, boolean isSigForOPS) { + if (isSigForOPS) { + signatures.addOnePassCorrespondingSignature(signature); + } else { + signatures.addPrependedSignature(signature); + } + } + + private void processSignatures(PGPSignatureList signatureList, boolean isSigForOPS) throws IOException { if (isSigForOPS) { signatures.addOnePassCorrespondingSignatures(signatureList); } else { @@ -192,21 +224,21 @@ public class OpenPgpMessageInputStream extends InputStream { } private void processCompressedData() throws IOException, PGPException { - PGPCompressedData compressedData = new PGPCompressedData(bcpgIn); + PGPCompressedData compressedData = new PGPCompressedData(packetInputStream); MessageMetadata.CompressedData compressionLayer = new MessageMetadata.CompressedData( CompressionAlgorithm.fromId(compressedData.getAlgorithm())); - in = new OpenPgpMessageInputStream(compressedData.getDataStream(), options, compressionLayer); + nestedInputStream = new OpenPgpMessageInputStream(compressedData.getDataStream(), options, compressionLayer); } private void processLiteralData() throws IOException { - PGPLiteralData literalData = new PGPLiteralData(bcpgIn); + PGPLiteralData literalData = new PGPLiteralData(packetInputStream); this.metadata.setChild(new MessageMetadata.LiteralData(literalData.getFileName(), literalData.getModificationTime(), StreamEncoding.requireFromCode(literalData.getFormat()))); - in = literalData.getDataStream(); + nestedInputStream = literalData.getDataStream(); } private boolean processEncryptedData() throws IOException, PGPException { - PGPEncryptedDataList encDataList = new PGPEncryptedDataList(bcpgIn); + PGPEncryptedDataList encDataList = new PGPEncryptedDataList(packetInputStream); // TODO: Replace with !encDataList.isIntegrityProtected() if (!encDataList.get(0).isIntegrityProtected()) { @@ -225,11 +257,11 @@ public class OpenPgpMessageInputStream extends InputStream { MessageMetadata.EncryptedData encryptedData = new MessageMetadata.EncryptedData(options.getSessionKey().getAlgorithm()); if (esk instanceof PGPPBEEncryptedData) { PGPPBEEncryptedData skesk = (PGPPBEEncryptedData) esk; - in = new OpenPgpMessageInputStream(skesk.getDataStream(decryptorFactory), options, encryptedData); + nestedInputStream = new OpenPgpMessageInputStream(skesk.getDataStream(decryptorFactory), options, encryptedData); return true; } else if (esk instanceof PGPPublicKeyEncryptedData) { PGPPublicKeyEncryptedData pkesk = (PGPPublicKeyEncryptedData) esk; - in = new OpenPgpMessageInputStream(pkesk.getDataStream(decryptorFactory), options, encryptedData); + nestedInputStream = new OpenPgpMessageInputStream(pkesk.getDataStream(decryptorFactory), options, encryptedData); return true; } else { throw new RuntimeException("Unknown ESK class type: " + esk.getClass().getName()); @@ -248,7 +280,7 @@ public class OpenPgpMessageInputStream extends InputStream { InputStream decrypted = skesk.getDataStream(decryptorFactory); MessageMetadata.EncryptedData encryptedData = new MessageMetadata.EncryptedData( SymmetricKeyAlgorithm.requireFromId(skesk.getSymmetricAlgorithm(decryptorFactory))); - in = new OpenPgpMessageInputStream(decrypted, options, encryptedData); + nestedInputStream = new OpenPgpMessageInputStream(decrypted, options, encryptedData); return true; } catch (PGPException e) { // password mismatch? Try next password @@ -274,7 +306,7 @@ public class OpenPgpMessageInputStream extends InputStream { InputStream decrypted = pkesk.getDataStream(decryptorFactory); MessageMetadata.EncryptedData encryptedData = new MessageMetadata.EncryptedData( SymmetricKeyAlgorithm.requireFromId(pkesk.getSymmetricAlgorithm(decryptorFactory))); - in = new OpenPgpMessageInputStream(decrypted, options, encryptedData); + nestedInputStream = new OpenPgpMessageInputStream(decrypted, options, encryptedData); return true; } catch (PGPException e) { // hm :/ @@ -293,7 +325,7 @@ public class OpenPgpMessageInputStream extends InputStream { InputStream decrypted = pkesk.getDataStream(decryptorFactory); MessageMetadata.EncryptedData encryptedData = new MessageMetadata.EncryptedData( SymmetricKeyAlgorithm.requireFromId(pkesk.getSymmetricAlgorithm(decryptorFactory))); - in = new OpenPgpMessageInputStream(decrypted, options, encryptedData); + nestedInputStream = new OpenPgpMessageInputStream(decrypted, options, encryptedData); return true; } catch (PGPException e) { // hm :/ @@ -307,7 +339,7 @@ public class OpenPgpMessageInputStream extends InputStream { private int nextTag() throws IOException { try { - return bcpgIn.nextPacketTag(); + return packetInputStream.nextPacketTag(); } catch (IOException e) { if ("Stream closed".equals(e.getMessage())) { // ZipInflater Streams sometimes close under our feet -.- @@ -345,13 +377,23 @@ public class OpenPgpMessageInputStream extends InputStream { return null; } + private PGPOnePassSignature readOnePassSignature() + throws PGPException, IOException { + return new PGPOnePassSignature(packetInputStream); + } + + private PGPSignature readSignature() + throws PGPException, IOException { + return new PGPSignature(packetInputStream); + } + private PGPOnePassSignatureListWrapper readOnePassSignatures() throws IOException { List encapsulating = new ArrayList<>(); ByteArrayOutputStream buf = new ByteArrayOutputStream(); BCPGOutputStream bcpgOut = new BCPGOutputStream(buf); int tag; while ((tag = nextTag()) == PacketTags.ONE_PASS_SIGNATURE || tag == PacketTags.MARKER) { - Packet packet = bcpgIn.readPacket(); + Packet packet = packetInputStream.readPacket(); if (tag == PacketTags.ONE_PASS_SIGNATURE) { OnePassSignaturePacket sigPacket = (OnePassSignaturePacket) packet; byte[] bytes = sigPacket.getEncoded(); @@ -371,7 +413,7 @@ public class OpenPgpMessageInputStream extends InputStream { BCPGOutputStream bcpgOut = new BCPGOutputStream(buf); int tag = nextTag(); while (tag == PacketTags.SIGNATURE || tag == PacketTags.MARKER) { - Packet packet = bcpgIn.readPacket(); + Packet packet = packetInputStream.readPacket(); if (tag == PacketTags.SIGNATURE) { SignaturePacket sigPacket = (SignaturePacket) packet; sigPacket.encode(bcpgOut); @@ -387,14 +429,14 @@ public class OpenPgpMessageInputStream extends InputStream { @Override public int read() throws IOException { - if (in == null) { + if (nestedInputStream == null) { automaton.assertValid(); return -1; } int r; try { - r = in.read(); + r = nestedInputStream.read(); } catch (IOException e) { r = -1; } @@ -403,9 +445,9 @@ public class OpenPgpMessageInputStream extends InputStream { byte b = (byte) r; signatures.update(b); } else { - in.close(); + nestedInputStream.close(); collectMetadata(); - in = null; + nestedInputStream = null; try { consumePackets(); @@ -421,16 +463,16 @@ public class OpenPgpMessageInputStream extends InputStream { public int read(byte[] b, int off, int len) throws IOException { - if (in == null) { + if (nestedInputStream == null) { automaton.assertValid(); return -1; } - int r = in.read(b, off, len); + int r = nestedInputStream.read(b, off, len); if (r == -1) { - in.close(); + nestedInputStream.close(); collectMetadata(); - in = null; + nestedInputStream = null; try { consumePackets(); @@ -449,10 +491,10 @@ public class OpenPgpMessageInputStream extends InputStream { return; } - if (in != null) { - in.close(); + if (nestedInputStream != null) { + nestedInputStream.close(); collectMetadata(); - in = null; + nestedInputStream = null; } try { @@ -467,8 +509,8 @@ public class OpenPgpMessageInputStream extends InputStream { } private void collectMetadata() { - if (in instanceof OpenPgpMessageInputStream) { - OpenPgpMessageInputStream child = (OpenPgpMessageInputStream) in; + if (nestedInputStream instanceof OpenPgpMessageInputStream) { + OpenPgpMessageInputStream child = (OpenPgpMessageInputStream) nestedInputStream; MessageMetadata.Layer childLayer = child.metadata; this.metadata.setChild((MessageMetadata.Nested) childLayer); } @@ -533,13 +575,14 @@ public class OpenPgpMessageInputStream extends InputStream { } } - private static final class Signatures { + private static final class Signatures extends OutputStream { final ConsumerOptions options; List detachedSignatures = new ArrayList<>(); List prependedSignatures = new ArrayList<>(); - List onePassSignatures = new ArrayList<>(); + List> onePassSignatures = new ArrayList<>(); List correspondingSignatures = new ArrayList<>(); + boolean lastOpsIsContaining = true; private Signatures(ConsumerOptions options) { this.options = options; @@ -547,28 +590,44 @@ public class OpenPgpMessageInputStream extends InputStream { void addDetachedSignatures(Collection signatures) { for (PGPSignature signature : signatures) { - long keyId = SignatureUtils.determineIssuerKeyId(signature); - PGPPublicKeyRing certificate = findCertificate(keyId); - initialize(signature, certificate, keyId); + addDetachedSignature(signature); } - this.detachedSignatures.addAll(signatures); + } + + void addDetachedSignature(PGPSignature signature) { + long keyId = SignatureUtils.determineIssuerKeyId(signature); + PGPPublicKeyRing certificate = findCertificate(keyId); + initialize(signature, certificate, keyId); + this.detachedSignatures.add(signature); } void addPrependedSignatures(PGPSignatureList signatures) { for (PGPSignature signature : signatures) { - long keyId = SignatureUtils.determineIssuerKeyId(signature); - PGPPublicKeyRing certificate = findCertificate(keyId); - initialize(signature, certificate, keyId); - this.prependedSignatures.add(signature); + addPrependedSignature(signature); } } - void addOnePassSignatures(PGPOnePassSignatureListWrapper signatures) { - for (PGPOnePassSignature ops : signatures.list) { - PGPPublicKeyRing certificate = findCertificate(ops.getKeyID()); - initialize(ops, certificate); - this.onePassSignatures.add(ops); + void addPrependedSignature(PGPSignature signature) { + long keyId = SignatureUtils.determineIssuerKeyId(signature); + PGPPublicKeyRing certificate = findCertificate(keyId); + initialize(signature, certificate, keyId); + this.prependedSignatures.add(signature); + } + + void addOnePassSignature(PGPOnePassSignature signature) { + List list; + if (lastOpsIsContaining) { + list = new ArrayList<>(); + onePassSignatures.add(list); + } else { + list = onePassSignatures.get(onePassSignatures.size() - 1); } + + PGPPublicKeyRing certificate = findCertificate(signature.getKeyID()); + initialize(signature, certificate); + list.add(signature); + + // lastOpsIsContaining = signature.isContaining(); } void addOnePassCorrespondingSignatures(PGPSignatureList signatures) { @@ -618,8 +677,10 @@ public class OpenPgpMessageInputStream extends InputStream { for (PGPSignature prepended : prependedSignatures) { prepended.update(b); } - for (PGPOnePassSignature ops : onePassSignatures) { - ops.update(b); + for (List opss : onePassSignatures) { + for (PGPOnePassSignature ops : opss) { + ops.update(b); + } } for (PGPSignature detached : detachedSignatures) { detached.update(b); @@ -660,5 +721,10 @@ public class OpenPgpMessageInputStream extends InputStream { LOGGER.debug("One-Pass-Signature by " + Long.toHexString(ops.getKeyID()) + " is " + (verified ? "verified" : "unverified")); } } + + @Override + public void write(int b) throws IOException { + update((byte) b); + } } } From c40a7976e224fe26de932c9f47f7dcfba97e6723 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 28 Sep 2022 17:38:20 +0200 Subject: [PATCH 14/80] Wip --- .../OpenPgpMessageInputStream.java | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) 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 df3d6805..36de2635 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 @@ -60,6 +60,7 @@ import java.io.OutputStream; import java.util.ArrayList; import java.util.Collection; import java.util.List; +import java.util.Stack; public class OpenPgpMessageInputStream extends InputStream { @@ -577,15 +578,20 @@ public class OpenPgpMessageInputStream extends InputStream { private static final class Signatures extends OutputStream { final ConsumerOptions options; - List detachedSignatures = new ArrayList<>(); - List prependedSignatures = new ArrayList<>(); - List> onePassSignatures = new ArrayList<>(); - List correspondingSignatures = new ArrayList<>(); + final List detachedSignatures; + final List prependedSignatures; + final Stack> onePassSignatures; + final List correspondingSignatures; boolean lastOpsIsContaining = true; private Signatures(ConsumerOptions options) { this.options = options; + this.detachedSignatures = new ArrayList<>(); + this.prependedSignatures = new ArrayList<>(); + this.onePassSignatures = new Stack<>(); + onePassSignatures.push(new ArrayList<>()); + this.correspondingSignatures = new ArrayList<>(); } void addDetachedSignatures(Collection signatures) { From 9ba3fcd8b0997b1acabd5af21e2c51b272f81cea Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 29 Sep 2022 00:15:18 +0200 Subject: [PATCH 15/80] SIGNATURE VERIFICATION IN OPENPGP SUCKS BIG TIME --- .../OpenPgpMessageInputStream.java | 198 ++++++++++-------- .../OpenPgpMessageInputStreamTest.java | 22 +- 2 files changed, 128 insertions(+), 92 deletions(-) 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 36de2635..a1364997 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 @@ -48,6 +48,7 @@ import org.pgpainless.key.info.KeyRingInfo; import org.pgpainless.key.protection.SecretKeyRingProtector; import org.pgpainless.key.protection.UnlockSecretKey; import org.pgpainless.signature.SignatureUtils; +import org.pgpainless.util.ArmorUtils; import org.pgpainless.util.Passphrase; import org.pgpainless.util.Tuple; import org.slf4j.Logger; @@ -138,19 +139,25 @@ public class OpenPgpMessageInputStream extends InputStream { // Literal Data - the literal data content is the new input stream case LIT: automaton.next(InputAlphabet.LiteralData); + signatures.commitNested(); processLiteralData(); break loop; // Compressed Data - the content contains another OpenPGP message case COMP: automaton.next(InputAlphabet.CompressedData); + signatures.commitNested(); processCompressedData(); break loop; // One Pass Signature case OPS: automaton.next(InputAlphabet.OnePassSignatures); - signatures.addOnePassSignature(readOnePassSignature()); + // signatures.addOnePassSignature(readOnePassSignature()); + PGPOnePassSignatureList onePassSignatureList = readOnePassSignatures(); + for (PGPOnePassSignature ops : onePassSignatureList) { + signatures.addOnePassSignature(ops); + } // signatures.addOnePassSignatures(readOnePassSignatures()); break; @@ -158,12 +165,15 @@ public class OpenPgpMessageInputStream extends InputStream { case SIG: boolean isSigForOPS = automaton.peekStack() == StackAlphabet.ops; automaton.next(InputAlphabet.Signatures); - PGPSignature signature = readSignature(); - processSignature(signature, isSigForOPS); - /* + + // PGPSignature signature = readSignature(); + // processSignature(signature, isSigForOPS); + PGPSignatureList signatureList = readSignatures(); - processSignatures(signatureList, isSigForOPS); - */ + for (PGPSignature signature : signatureList) { + processSignature(signature, isSigForOPS); + } + break; // Encrypted Data (ESKs and SED/SEIPD are parsed the same by BC) @@ -178,7 +188,7 @@ public class OpenPgpMessageInputStream extends InputStream { throw new MissingDecryptionMethodException("No working decryption method found."); - // Marker Packets need to be skipped and ignored + // Marker Packets need to be skipped and ignored case MARKER: packetInputStream.readPacket(); // skip break; @@ -193,8 +203,8 @@ public class OpenPgpMessageInputStream extends InputStream { case UATTR: throw new MalformedOpenPgpMessageException("Illegal Packet in Stream: " + nextPacket); - // MDC packet is usually processed by PGPEncryptedDataList, so it is very likely we encounter this - // packet out of order + // MDC packet is usually processed by PGPEncryptedDataList, so it is very likely we encounter this + // packet out of order case MDC: throw new MalformedOpenPgpMessageException("Unexpected Packet in Stream: " + nextPacket); @@ -210,20 +220,12 @@ public class OpenPgpMessageInputStream extends InputStream { private void processSignature(PGPSignature signature, boolean isSigForOPS) { if (isSigForOPS) { - signatures.addOnePassCorrespondingSignature(signature); + signatures.addCorrespondingOnePassSignature(signature); } else { signatures.addPrependedSignature(signature); } } - private void processSignatures(PGPSignatureList signatureList, boolean isSigForOPS) throws IOException { - if (isSigForOPS) { - signatures.addOnePassCorrespondingSignatures(signatureList); - } else { - signatures.addPrependedSignatures(signatureList); - } - } - private void processCompressedData() throws IOException, PGPException { PGPCompressedData compressedData = new PGPCompressedData(packetInputStream); MessageMetadata.CompressedData compressionLayer = new MessageMetadata.CompressedData( @@ -380,16 +382,17 @@ public class OpenPgpMessageInputStream extends InputStream { private PGPOnePassSignature readOnePassSignature() throws PGPException, IOException { - return new PGPOnePassSignature(packetInputStream); + //return new PGPOnePassSignature(packetInputStream); + return null; } private PGPSignature readSignature() throws PGPException, IOException { - return new PGPSignature(packetInputStream); + //return new PGPSignature(packetInputStream); + return null; } - private PGPOnePassSignatureListWrapper readOnePassSignatures() throws IOException { - List encapsulating = new ArrayList<>(); + private PGPOnePassSignatureList readOnePassSignatures() throws IOException { ByteArrayOutputStream buf = new ByteArrayOutputStream(); BCPGOutputStream bcpgOut = new BCPGOutputStream(buf); int tag; @@ -398,7 +401,6 @@ public class OpenPgpMessageInputStream extends InputStream { if (tag == PacketTags.ONE_PASS_SIGNATURE) { OnePassSignaturePacket sigPacket = (OnePassSignaturePacket) packet; byte[] bytes = sigPacket.getEncoded(); - encapsulating.add(bytes[bytes.length - 1] == 1); bcpgOut.write(bytes); } } @@ -406,7 +408,7 @@ public class OpenPgpMessageInputStream extends InputStream { PGPObjectFactory objectFactory = ImplementationFactory.getInstance().getPGPObjectFactory(buf.toByteArray()); PGPOnePassSignatureList signatureList = (PGPOnePassSignatureList) objectFactory.nextObject(); - return new PGPOnePassSignatureListWrapper(signatureList, encapsulating); + return signatureList; } private PGPSignatureList readSignatures() throws IOException { @@ -449,6 +451,7 @@ public class OpenPgpMessageInputStream extends InputStream { nestedInputStream.close(); collectMetadata(); nestedInputStream = null; + signatures.popNested(); try { consumePackets(); @@ -474,6 +477,7 @@ public class OpenPgpMessageInputStream extends InputStream { nestedInputStream.close(); collectMetadata(); nestedInputStream = null; + signatures.popNested(); try { consumePackets(); @@ -556,41 +560,29 @@ public class OpenPgpMessageInputStream extends InputStream { } } - /** - * Workaround for BC not exposing, whether an OPS is encapsulating or not. - * TODO: Remove once our PR is merged - * - * @see PR against BC - */ - private static class PGPOnePassSignatureListWrapper { - private final PGPOnePassSignatureList list; - private final List encapsulating; - - PGPOnePassSignatureListWrapper(PGPOnePassSignatureList signatures, List encapsulating) { - this.list = signatures; - this.encapsulating = encapsulating; - } - - public int size() { - return list.size(); - } - } - + // TODO: In 'OPS LIT("Foo") SIG', OPS is only updated with "Foo" + // In 'OPS[1] OPS LIT("Foo") SIG SIG', OPS[1] (nested) is updated with OPS LIT("Foo") SIG. + // Therefore, we need to handle the innermost signature layer differently when updating with Literal data. + // For this we might want to provide two update entries into the Signatures class, one for OpenPGP packets and one + // for literal data. UUUUUGLY!!!! private static final class Signatures extends OutputStream { final ConsumerOptions options; final List detachedSignatures; final List prependedSignatures; - final Stack> onePassSignatures; + final List onePassSignatures; + final Stack> opsUpdateStack; final List correspondingSignatures; - boolean lastOpsIsContaining = true; + ByteArrayOutputStream out = new ByteArrayOutputStream(); + + List opsCurrentNesting = new ArrayList<>(); private Signatures(ConsumerOptions options) { this.options = options; this.detachedSignatures = new ArrayList<>(); this.prependedSignatures = new ArrayList<>(); - this.onePassSignatures = new Stack<>(); - onePassSignatures.push(new ArrayList<>()); + this.onePassSignatures = new ArrayList<>(); + this.opsUpdateStack = new Stack<>(); this.correspondingSignatures = new ArrayList<>(); } @@ -607,12 +599,6 @@ public class OpenPgpMessageInputStream extends InputStream { this.detachedSignatures.add(signature); } - void addPrependedSignatures(PGPSignatureList signatures) { - for (PGPSignature signature : signatures) { - addPrependedSignature(signature); - } - } - void addPrependedSignature(PGPSignature signature) { long keyId = SignatureUtils.determineIssuerKeyId(signature); PGPPublicKeyRing certificate = findCertificate(keyId); @@ -621,27 +607,61 @@ public class OpenPgpMessageInputStream extends InputStream { } void addOnePassSignature(PGPOnePassSignature signature) { - List list; - if (lastOpsIsContaining) { - list = new ArrayList<>(); - onePassSignatures.add(list); - } else { - list = onePassSignatures.get(onePassSignatures.size() - 1); - } - PGPPublicKeyRing certificate = findCertificate(signature.getKeyID()); initialize(signature, certificate); - list.add(signature); + onePassSignatures.add(signature); - // lastOpsIsContaining = signature.isContaining(); + opsCurrentNesting.add(signature); + if (isContaining(signature)) { + commitNested(); + } } - void addOnePassCorrespondingSignatures(PGPSignatureList signatures) { - for (PGPSignature signature : signatures) { - correspondingSignatures.add(signature); + boolean isContaining(PGPOnePassSignature ops) { + try { + byte[] bytes = ops.getEncoded(); + return bytes[bytes.length - 1] == 1; + } catch (IOException e) { + return false; } } + void addCorrespondingOnePassSignature(PGPSignature signature) { + for (PGPOnePassSignature onePassSignature : onePassSignatures) { + if (onePassSignature.getKeyID() != signature.getKeyID()) { + continue; + } + + boolean verified = false; + try { + verified = onePassSignature.verify(signature); + } catch (PGPException e) { + log("Cannot verify OPS signature.", e); + } + log("One-Pass-Signature by " + Long.toHexString(onePassSignature.getKeyID()) + " is " + (verified ? "verified" : "unverified")); + try { + log(ArmorUtils.toAsciiArmoredString(out.toByteArray())); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + } + + void commitNested() { + if (opsCurrentNesting.isEmpty()) { + return; + } + + log("Committing " + opsCurrentNesting.size() + " OPS sigs for updating"); + opsUpdateStack.push(opsCurrentNesting); + opsCurrentNesting = new ArrayList<>(); + } + + void popNested() { + log("Popping nested"); + opsUpdateStack.pop(); + } + private void initialize(PGPSignature signature, PGPPublicKeyRing certificate, long keyId) { if (certificate == null) { // SHIT @@ -680,14 +700,21 @@ public class OpenPgpMessageInputStream extends InputStream { } public void update(byte b) { + if (!opsUpdateStack.isEmpty()) { + log("Update"); + out.write(b); + } + for (PGPSignature prepended : prependedSignatures) { prepended.update(b); } - for (List opss : onePassSignatures) { + + for (List opss : opsUpdateStack) { for (PGPOnePassSignature ops : opss) { ops.update(b); } } + for (PGPSignature detached : detachedSignatures) { detached.update(b); } @@ -699,9 +726,9 @@ public class OpenPgpMessageInputStream extends InputStream { try { verified = detached.verify(); } catch (PGPException e) { - LOGGER.debug("Cannot verify detached signature.", e); + log("Cannot verify detached signature.", e); } - LOGGER.debug("Detached Signature by " + Long.toHexString(detached.getKeyID()) + " is " + (verified ? "verified" : "unverified")); + log("Detached Signature by " + Long.toHexString(detached.getKeyID()) + " is " + (verified ? "verified" : "unverified")); } for (PGPSignature prepended : prependedSignatures) { @@ -709,22 +736,9 @@ public class OpenPgpMessageInputStream extends InputStream { try { verified = prepended.verify(); } catch (PGPException e) { - LOGGER.debug("Cannot verify prepended signature.", e); + log("Cannot verify prepended signature.", e); } - LOGGER.debug("Prepended Signature by " + Long.toHexString(prepended.getKeyID()) + " is " + (verified ? "verified" : "unverified")); - } - - - for (int i = 0; i < onePassSignatures.size(); i++) { - PGPOnePassSignature ops = onePassSignatures.get(i); - PGPSignature signature = correspondingSignatures.get(correspondingSignatures.size() - i - 1); - boolean verified = false; - try { - verified = ops.verify(signature); - } catch (PGPException e) { - LOGGER.debug("Cannot verify OPS signature.", e); - } - LOGGER.debug("One-Pass-Signature by " + Long.toHexString(ops.getKeyID()) + " is " + (verified ? "verified" : "unverified")); + log("Prepended Signature by " + Long.toHexString(prepended.getKeyID()) + " is " + (verified ? "verified" : "unverified")); } } @@ -733,4 +747,18 @@ public class OpenPgpMessageInputStream extends InputStream { update((byte) b); } } + + static void log(String message) { + LOGGER.debug(message); + // CHECKSTYLE:OFF + System.out.println(message); + // CHECKSTYLE:ON + } + + static void log(String message, Throwable e) { + log(message); + // CHECKSTYLE:OFF + e.printStackTrace(); + // CHECKSTYLE:ON + } } 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 6962e279..e52a6016 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 @@ -4,6 +4,7 @@ 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 static org.junit.jupiter.api.Assertions.assertTrue; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; @@ -377,7 +378,8 @@ public class OpenPgpMessageInputStreamTest { @ParameterizedTest(name = "Process SIG COMP(LIT) using {0}") @MethodSource("provideMessageProcessors") - public void testProcessSIG_COMP_LIT(Processor processor) throws PGPException, IOException { + public void testProcessSIG_COMP_LIT(Processor processor) + throws PGPException, IOException { PGPPublicKeyRing cert = PGPainless.extractCertificate( PGPainless.readKeyRing().secretKeyRing(KEY)); @@ -392,7 +394,8 @@ public class OpenPgpMessageInputStreamTest { @ParameterizedTest(name = "Process SENC(LIT) using {0}") @MethodSource("provideMessageProcessors") - public void testProcessSENC_LIT(Processor processor) throws PGPException, IOException { + 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(); @@ -404,7 +407,8 @@ public class OpenPgpMessageInputStreamTest { @ParameterizedTest(name = "Process PENC(COMP(LIT)) using {0}") @MethodSource("provideMessageProcessors") - public void testProcessPENC_COMP_LIT(Processor processor) throws IOException, PGPException { + public void testProcessPENC_COMP_LIT(Processor processor) + throws IOException, PGPException { PGPSecretKeyRing secretKeys = PGPainless.readKeyRing().secretKeyRing(KEY); Tuple result = processor.process(PENC_COMP_LIT, ConsumerOptions.get() .addDecryptionKey(secretKeys)); @@ -417,7 +421,8 @@ public class OpenPgpMessageInputStreamTest { @ParameterizedTest(name = "Process OPS LIT SIG using {0}") @MethodSource("provideMessageProcessors") - public void testProcessOPS_LIT_SIG(Processor processor) throws IOException, PGPException { + public void testProcessOPS_LIT_SIG(Processor processor) + throws IOException, PGPException { PGPPublicKeyRing cert = PGPainless.extractCertificate(PGPainless.readKeyRing().secretKeyRing(KEY)); Tuple result = processor.process(OPS_LIT_SIG, ConsumerOptions.get() .addVerificationCert(cert)); @@ -428,7 +433,8 @@ public class OpenPgpMessageInputStreamTest { assertNull(metadata.getCompressionAlgorithm()); } - private static Tuple 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); @@ -437,7 +443,8 @@ public class OpenPgpMessageInputStreamTest { return new Tuple<>(out.toString(), metadata); } - private static Tuple 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(); @@ -451,7 +458,8 @@ public class OpenPgpMessageInputStreamTest { return new Tuple<>(out.toString(), metadata); } - private static 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); From fe767389a0f41dec7b23df3b692a27121c00410b Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 29 Sep 2022 12:33:08 +0200 Subject: [PATCH 16/80] Fix ModificationDetectionException by not calling PGPUtil.getDecoderStream() --- .../DecryptionStreamFactory.java | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) 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 a787280a..07be7c10 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,7 +36,6 @@ 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; import org.bouncycastle.openpgp.operator.PublicKeyDataDecryptorFactory; @@ -241,14 +240,12 @@ public final class DecryptionStreamFactory { SessionKey sessionKey = options.getSessionKey(); if (sessionKey != null) { integrityProtectedEncryptedInputStream = decryptWithProvidedSessionKey(pgpEncryptedDataList, sessionKey); - InputStream decodedDataStream = PGPUtil.getDecoderStream(integrityProtectedEncryptedInputStream); - PGPObjectFactory factory = ImplementationFactory.getInstance().getPGPObjectFactory(decodedDataStream); + PGPObjectFactory factory = ImplementationFactory.getInstance().getPGPObjectFactory(integrityProtectedEncryptedInputStream); return processPGPPackets(factory, ++depth); } InputStream decryptedDataStream = decryptSessionKey(pgpEncryptedDataList); - InputStream decodedDataStream = PGPUtil.getDecoderStream(decryptedDataStream); - PGPObjectFactory factory = ImplementationFactory.getInstance().getPGPObjectFactory(decodedDataStream); + PGPObjectFactory factory = ImplementationFactory.getInstance().getPGPObjectFactory(decryptedDataStream); return processPGPPackets(factory, ++depth); } @@ -299,8 +296,7 @@ public final class DecryptionStreamFactory { } InputStream inflatedDataStream = pgpCompressedData.getDataStream(); - InputStream decodedDataStream = PGPUtil.getDecoderStream(inflatedDataStream); - PGPObjectFactory objectFactory = ImplementationFactory.getInstance().getPGPObjectFactory(decodedDataStream); + PGPObjectFactory objectFactory = ImplementationFactory.getInstance().getPGPObjectFactory(inflatedDataStream); return processPGPPackets(objectFactory, ++depth); } From be8439532d5990e39bc53d6f790367230cf81499 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 29 Sep 2022 12:50:32 +0200 Subject: [PATCH 17/80] Use BCs Arrays.constantTimeAreEqual(char[], char[]) --- .../main/java/org/pgpainless/util/BCUtil.java | 54 ------------------- .../java/org/pgpainless/util/Passphrase.java | 4 +- .../java/org/pgpainless/util/BCUtilTest.java | 17 +++--- 3 files changed, 10 insertions(+), 65 deletions(-) delete mode 100644 pgpainless-core/src/main/java/org/pgpainless/util/BCUtil.java diff --git a/pgpainless-core/src/main/java/org/pgpainless/util/BCUtil.java b/pgpainless-core/src/main/java/org/pgpainless/util/BCUtil.java deleted file mode 100644 index bb811cea..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/util/BCUtil.java +++ /dev/null @@ -1,54 +0,0 @@ -// SPDX-FileCopyrightText: 2018 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.util; - -public final class BCUtil { - - private BCUtil() { - - } - - /** - * A constant time equals comparison - does not terminate early if - * test will fail. For best results always pass the expected value - * as the first parameter. - * - * TODO: This method was proposed as a patch to BC: - * https://github.com/bcgit/bc-java/pull/1141 - * Replace usage of this method with upstream eventually. - * Remove once BC 172 gets released, given it contains the patch. - * - * @param expected first array - * @param supplied second array - * @return true if arrays equal, false otherwise. - */ - public static boolean constantTimeAreEqual( - char[] expected, - char[] supplied) { - if (expected == null || supplied == null) { - return false; - } - - if (expected == supplied) { - return true; - } - - int len = Math.min(expected.length, supplied.length); - - int nonEqual = expected.length ^ supplied.length; - - // do the char-wise comparison - for (int i = 0; i != len; i++) { - nonEqual |= (expected[i] ^ supplied[i]); - } - // If supplied is longer than expected, iterate over rest of supplied with NOPs - for (int i = len; i < supplied.length; i++) { - nonEqual |= ((byte) supplied[i] ^ (byte) ~supplied[i]); - } - - return nonEqual == 0; - } - -} diff --git a/pgpainless-core/src/main/java/org/pgpainless/util/Passphrase.java b/pgpainless-core/src/main/java/org/pgpainless/util/Passphrase.java index 4cef145a..9576fb3e 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/util/Passphrase.java +++ b/pgpainless-core/src/main/java/org/pgpainless/util/Passphrase.java @@ -8,8 +8,6 @@ import javax.annotation.Nonnull; import javax.annotation.Nullable; import java.util.Arrays; -import static org.pgpainless.util.BCUtil.constantTimeAreEqual; - public class Passphrase { public final Object lock = new Object(); @@ -165,6 +163,6 @@ public class Passphrase { } Passphrase other = (Passphrase) obj; return (getChars() == null && other.getChars() == null) || - constantTimeAreEqual(getChars(), other.getChars()); + org.bouncycastle.util.Arrays.constantTimeAreEqual(getChars(), other.getChars()); } } diff --git a/pgpainless-core/src/test/java/org/pgpainless/util/BCUtilTest.java b/pgpainless-core/src/test/java/org/pgpainless/util/BCUtilTest.java index 6fb90e63..82368762 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/util/BCUtilTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/util/BCUtilTest.java @@ -19,6 +19,7 @@ import org.bouncycastle.openpgp.PGPPublicKeyRing; import org.bouncycastle.openpgp.PGPPublicKeyRingCollection; import org.bouncycastle.openpgp.PGPSecretKeyRing; import org.bouncycastle.openpgp.PGPSecretKeyRingCollection; +import org.bouncycastle.util.Arrays; import org.junit.jupiter.api.Test; import org.pgpainless.PGPainless; import org.pgpainless.algorithm.KeyFlag; @@ -94,14 +95,14 @@ public class BCUtilTest { @Test public void constantTimeAreEqualsTest() { char[] b = "Hello".toCharArray(); - assertTrue(BCUtil.constantTimeAreEqual(b, b)); - assertTrue(BCUtil.constantTimeAreEqual("Hello".toCharArray(), "Hello".toCharArray())); - assertTrue(BCUtil.constantTimeAreEqual(new char[0], new char[0])); - assertTrue(BCUtil.constantTimeAreEqual(new char[] {'H', 'e', 'l', 'l', 'o'}, "Hello".toCharArray())); + assertTrue(Arrays.constantTimeAreEqual(b, b)); + assertTrue(Arrays.constantTimeAreEqual("Hello".toCharArray(), "Hello".toCharArray())); + assertTrue(Arrays.constantTimeAreEqual(new char[0], new char[0])); + assertTrue(Arrays.constantTimeAreEqual(new char[] {'H', 'e', 'l', 'l', 'o'}, "Hello".toCharArray())); - assertFalse(BCUtil.constantTimeAreEqual("Hello".toCharArray(), "Hello World".toCharArray())); - assertFalse(BCUtil.constantTimeAreEqual(null, "Hello".toCharArray())); - assertFalse(BCUtil.constantTimeAreEqual("Hello".toCharArray(), null)); - assertFalse(BCUtil.constantTimeAreEqual(null, null)); + assertFalse(Arrays.constantTimeAreEqual("Hello".toCharArray(), "Hello World".toCharArray())); + assertFalse(Arrays.constantTimeAreEqual(null, "Hello".toCharArray())); + assertFalse(Arrays.constantTimeAreEqual("Hello".toCharArray(), null)); + assertFalse(Arrays.constantTimeAreEqual((char[]) null, null)); } } From 6809a490c14f1ec0d944581de6283cf3914b6c1b Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 29 Sep 2022 13:02:22 +0200 Subject: [PATCH 18/80] Remove unnecessary throws declarations --- .../key/collection/PGPKeyRingCollection.java | 2 +- .../secretkeyring/SecretKeyRingEditor.java | 2 +- .../pgpainless/key/parsing/KeyRingReader.java | 6 ++---- .../org/pgpainless/key/util/KeyRingUtils.java | 17 +++-------------- .../ModificationDetectionTests.java | 2 +- .../EncryptionOptionsTest.java | 3 +-- .../certification/CertifyCertificateTest.java | 2 +- .../GenerateKeyWithAdditionalUserIdTest.java | 6 +++--- .../java/org/pgpainless/util/BCUtilTest.java | 4 +--- .../keyring/KeyRingsFromCollectionTest.java | 4 ++-- .../java/org/pgpainless/sop/DearmorImpl.java | 2 +- .../java/org/pgpainless/sop/InlineSignImpl.java | 2 +- .../org/pgpainless/sop/InlineVerifyImpl.java | 2 +- 13 files changed, 19 insertions(+), 35 deletions(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/collection/PGPKeyRingCollection.java b/pgpainless-core/src/main/java/org/pgpainless/key/collection/PGPKeyRingCollection.java index 46aa2baf..6cf7102d 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/key/collection/PGPKeyRingCollection.java +++ b/pgpainless-core/src/main/java/org/pgpainless/key/collection/PGPKeyRingCollection.java @@ -74,7 +74,7 @@ public class PGPKeyRingCollection { } public PGPKeyRingCollection(@Nonnull Collection collection, boolean isSilent) - throws IOException, PGPException { + throws PGPException { List secretKeyRings = new ArrayList<>(); List publicKeyRings = new ArrayList<>(); diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/modification/secretkeyring/SecretKeyRingEditor.java b/pgpainless-core/src/main/java/org/pgpainless/key/modification/secretkeyring/SecretKeyRingEditor.java index 20876c36..01c09903 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/key/modification/secretkeyring/SecretKeyRingEditor.java +++ b/pgpainless-core/src/main/java/org/pgpainless/key/modification/secretkeyring/SecretKeyRingEditor.java @@ -327,7 +327,7 @@ public class SecretKeyRingEditor implements SecretKeyRingEditorInterface { @Nonnull SecretKeyRingProtector primaryKeyProtector, @Nonnull KeyFlag keyFlag, KeyFlag... additionalKeyFlags) - throws PGPException, IOException, NoSuchAlgorithmException { + throws PGPException, IOException { KeyFlag[] flags = concat(keyFlag, additionalKeyFlags); PublicKeyAlgorithm subkeyAlgorithm = PublicKeyAlgorithm.requireFromId(subkey.getPublicKey().getAlgorithm()); SignatureSubpacketsUtil.assureKeyCanCarryFlags(subkeyAlgorithm); diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/parsing/KeyRingReader.java b/pgpainless-core/src/main/java/org/pgpainless/key/parsing/KeyRingReader.java index b5b8ea65..bbaecd4f 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/key/parsing/KeyRingReader.java +++ b/pgpainless-core/src/main/java/org/pgpainless/key/parsing/KeyRingReader.java @@ -232,10 +232,9 @@ public class KeyRingReader { * @return public key ring collection * * @throws IOException in case of an IO error or exceeding of max iterations - * @throws PGPException in case of a broken key */ public static PGPPublicKeyRingCollection readPublicKeyRingCollection(@Nonnull InputStream inputStream, int maxIterations) - throws IOException, PGPException { + throws IOException { PGPObjectFactory objectFactory = ImplementationFactory.getInstance().getPGPObjectFactory( ArmorUtils.getDecoderStream(inputStream)); @@ -317,11 +316,10 @@ public class KeyRingReader { * @return secret key ring collection * * @throws IOException in case of an IO error or exceeding of max iterations - * @throws PGPException in case of a broken secret key */ public static PGPSecretKeyRingCollection readSecretKeyRingCollection(@Nonnull InputStream inputStream, int maxIterations) - throws IOException, PGPException { + throws IOException { PGPObjectFactory objectFactory = ImplementationFactory.getInstance().getPGPObjectFactory( ArmorUtils.getDecoderStream(inputStream)); diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/util/KeyRingUtils.java b/pgpainless-core/src/main/java/org/pgpainless/key/util/KeyRingUtils.java index 0bcdf8d1..013e283e 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/key/util/KeyRingUtils.java +++ b/pgpainless-core/src/main/java/org/pgpainless/key/util/KeyRingUtils.java @@ -167,12 +167,9 @@ public final class KeyRingUtils { * * @param secretKeyRings secret key ring collection * @return public key ring collection - * @throws PGPException TODO: remove - * @throws IOException TODO: remove */ @Nonnull - public static PGPPublicKeyRingCollection publicKeyRingCollectionFrom(@Nonnull PGPSecretKeyRingCollection secretKeyRings) - throws PGPException, IOException { + public static PGPPublicKeyRingCollection publicKeyRingCollectionFrom(@Nonnull PGPSecretKeyRingCollection secretKeyRings) { List certificates = new ArrayList<>(); for (PGPSecretKeyRing secretKey : secretKeyRings) { certificates.add(PGPainless.extractCertificate(secretKey)); @@ -200,13 +197,9 @@ public final class KeyRingUtils { * * @param rings array of public key rings * @return key ring collection - * - * @throws IOException in case of an io error - * @throws PGPException in case of a broken key */ @Nonnull - public static PGPPublicKeyRingCollection keyRingsToKeyRingCollection(@Nonnull PGPPublicKeyRing... rings) - throws IOException, PGPException { + public static PGPPublicKeyRingCollection keyRingsToKeyRingCollection(@Nonnull PGPPublicKeyRing... rings) { return new PGPPublicKeyRingCollection(Arrays.asList(rings)); } @@ -215,13 +208,9 @@ public final class KeyRingUtils { * * @param rings array of secret key rings * @return secret key ring collection - * - * @throws IOException in case of an io error - * @throws PGPException in case of a broken key */ @Nonnull - public static PGPSecretKeyRingCollection keyRingsToKeyRingCollection(@Nonnull PGPSecretKeyRing... rings) - throws IOException, PGPException { + public static PGPSecretKeyRingCollection keyRingsToKeyRingCollection(@Nonnull PGPSecretKeyRing... rings) { return new PGPSecretKeyRingCollection(Arrays.asList(rings)); } diff --git a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/ModificationDetectionTests.java b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/ModificationDetectionTests.java index 2ca265ea..14a041ff 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/ModificationDetectionTests.java +++ b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/ModificationDetectionTests.java @@ -530,7 +530,7 @@ public class ModificationDetectionTests { ); } - private PGPSecretKeyRingCollection getDecryptionKey() throws IOException, PGPException { + private PGPSecretKeyRingCollection getDecryptionKey() throws IOException { PGPSecretKeyRing secretKeys = PGPainless.readKeyRing().secretKeyRing(keyAscii); return new PGPSecretKeyRingCollection(Collections.singletonList(secretKeys)); } diff --git a/pgpainless-core/src/test/java/org/pgpainless/encryption_signing/EncryptionOptionsTest.java b/pgpainless-core/src/test/java/org/pgpainless/encryption_signing/EncryptionOptionsTest.java index 3e7ccb4d..3436ba69 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/encryption_signing/EncryptionOptionsTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/encryption_signing/EncryptionOptionsTest.java @@ -9,7 +9,6 @@ import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; -import java.io.IOException; import java.security.InvalidAlgorithmParameterException; import java.security.NoSuchAlgorithmException; import java.util.Arrays; @@ -165,7 +164,7 @@ public class EncryptionOptionsTest { } @Test - public void testAddRecipients_PGPPublicKeyRingCollection() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, IOException { + public void testAddRecipients_PGPPublicKeyRingCollection() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException { PGPPublicKeyRing secondKeyRing = KeyRingUtils.publicKeyRingFrom( PGPainless.generateKeyRing().modernKeyRing("other@pgpainless.org")); diff --git a/pgpainless-core/src/test/java/org/pgpainless/key/certification/CertifyCertificateTest.java b/pgpainless-core/src/test/java/org/pgpainless/key/certification/CertifyCertificateTest.java index f837be1b..eb0069f5 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/key/certification/CertifyCertificateTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/key/certification/CertifyCertificateTest.java @@ -140,7 +140,7 @@ public class CertifyCertificateTest { } @Test - public void testScopedDelegation() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, IOException { + public void testScopedDelegation() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException { PGPSecretKeyRing aliceKey = PGPainless.generateKeyRing() .modernKeyRing("Alice "); PGPSecretKeyRing caKey = PGPainless.generateKeyRing() diff --git a/pgpainless-core/src/test/java/org/pgpainless/key/generation/GenerateKeyWithAdditionalUserIdTest.java b/pgpainless-core/src/test/java/org/pgpainless/key/generation/GenerateKeyWithAdditionalUserIdTest.java index 2e2ffc8d..e68d8e9b 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/key/generation/GenerateKeyWithAdditionalUserIdTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/key/generation/GenerateKeyWithAdditionalUserIdTest.java @@ -7,7 +7,6 @@ package org.pgpainless.key.generation; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; -import java.io.IOException; import java.security.InvalidAlgorithmParameterException; import java.security.NoSuchAlgorithmException; import java.util.Date; @@ -26,14 +25,15 @@ import org.pgpainless.key.generation.type.rsa.RsaLength; import org.pgpainless.key.util.KeyRingUtils; import org.pgpainless.key.util.UserId; import org.pgpainless.timeframe.TestTimeFrameProvider; +import org.pgpainless.util.DateUtil; import org.pgpainless.util.TestAllImplementations; public class GenerateKeyWithAdditionalUserIdTest { @TestTemplate @ExtendWith(TestAllImplementations.class) - public void test() throws InvalidAlgorithmParameterException, NoSuchAlgorithmException, PGPException, IOException { - Date now = new Date(); + public void test() throws InvalidAlgorithmParameterException, NoSuchAlgorithmException, PGPException { + Date now = DateUtil.now(); Date expiration = TestTimeFrameProvider.defaultExpirationForCreationDate(now); PGPSecretKeyRing secretKeys = PGPainless.buildKeyRing() .setPrimaryKey(KeySpec.getBuilder( diff --git a/pgpainless-core/src/test/java/org/pgpainless/util/BCUtilTest.java b/pgpainless-core/src/test/java/org/pgpainless/util/BCUtilTest.java index 82368762..821d9e30 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/util/BCUtilTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/util/BCUtilTest.java @@ -8,7 +8,6 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; -import java.io.IOException; import java.security.InvalidAlgorithmParameterException; import java.security.NoSuchAlgorithmException; import java.util.Iterator; @@ -36,8 +35,7 @@ public class BCUtilTest { @Test public void keyRingToCollectionTest() - throws PGPException, NoSuchAlgorithmException, InvalidAlgorithmParameterException, - IOException { + throws PGPException, NoSuchAlgorithmException, InvalidAlgorithmParameterException { PGPSecretKeyRing sec = PGPainless.buildKeyRing() .setPrimaryKey(KeySpec.getBuilder( KeyType.RSA(RsaLength._3072), diff --git a/pgpainless-core/src/test/java/org/pgpainless/util/selection/keyring/KeyRingsFromCollectionTest.java b/pgpainless-core/src/test/java/org/pgpainless/util/selection/keyring/KeyRingsFromCollectionTest.java index eb3510be..bf4955cd 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/util/selection/keyring/KeyRingsFromCollectionTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/util/selection/keyring/KeyRingsFromCollectionTest.java @@ -56,7 +56,7 @@ public class KeyRingsFromCollectionTest { } @Test - public void selectPublicKeyRingFromPublicKeyRingCollectionTest() throws IOException, PGPException { + public void selectPublicKeyRingFromPublicKeyRingCollectionTest() throws IOException { PGPPublicKeyRing emil = TestKeys.getEmilPublicKeyRing(); PGPPublicKeyRing juliet = TestKeys.getJulietPublicKeyRing(); PGPPublicKeyRingCollection collection = new PGPPublicKeyRingCollection(Arrays.asList(emil, juliet)); @@ -68,7 +68,7 @@ public class KeyRingsFromCollectionTest { } @Test - public void selectPublicKeyRingMapFromPublicKeyRingCollectionMapTest() throws IOException, PGPException { + public void selectPublicKeyRingMapFromPublicKeyRingCollectionMapTest() throws IOException { PGPPublicKeyRing emil = TestKeys.getEmilPublicKeyRing(); PGPPublicKeyRing juliet = TestKeys.getJulietPublicKeyRing(); MultiMap map = new MultiMap<>(); diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/DearmorImpl.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/DearmorImpl.java index ac53d415..f0b21a6d 100644 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/DearmorImpl.java +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/DearmorImpl.java @@ -18,7 +18,7 @@ import sop.operation.Dearmor; public class DearmorImpl implements Dearmor { @Override - public Ready data(InputStream data) throws IOException { + public Ready data(InputStream data) { InputStream decoder; try { decoder = PGPUtil.getDecoderStream(data); diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/InlineSignImpl.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/InlineSignImpl.java index 3bdc8fc3..99ad19e0 100644 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/InlineSignImpl.java +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/InlineSignImpl.java @@ -76,7 +76,7 @@ public class InlineSignImpl implements InlineSign { } @Override - public Ready data(InputStream data) throws SOPGPException.KeyIsProtected, IOException, SOPGPException.ExpectedText { + public Ready data(InputStream data) throws SOPGPException.KeyIsProtected, SOPGPException.ExpectedText { for (PGPSecretKeyRing key : signingKeys) { try { if (mode == InlineSignAs.CleartextSigned) { diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/InlineVerifyImpl.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/InlineVerifyImpl.java index 1e8c4fee..fe754e3b 100644 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/InlineVerifyImpl.java +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/InlineVerifyImpl.java @@ -53,7 +53,7 @@ public class InlineVerifyImpl implements InlineVerify { } @Override - public ReadyWithResult> data(InputStream data) throws IOException, SOPGPException.NoSignature, SOPGPException.BadData { + public ReadyWithResult> data(InputStream data) throws SOPGPException.NoSignature, SOPGPException.BadData { return new ReadyWithResult>() { @Override public List writeTo(OutputStream outputStream) throws IOException, SOPGPException.NoSignature { From 73cdf34b02e667ccb94563fdc82ca0fc94b3f27a Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 29 Sep 2022 13:02:36 +0200 Subject: [PATCH 19/80] DO NOT MERGE: Disable broken test --- .../java/org/pgpainless/signature/IgnoreMarkerPackets.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pgpainless-core/src/test/java/org/pgpainless/signature/IgnoreMarkerPackets.java b/pgpainless-core/src/test/java/org/pgpainless/signature/IgnoreMarkerPackets.java index c86efd05..87766dfe 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/signature/IgnoreMarkerPackets.java +++ b/pgpainless-core/src/test/java/org/pgpainless/signature/IgnoreMarkerPackets.java @@ -20,6 +20,7 @@ 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.Disabled; import org.junit.jupiter.api.Test; import org.pgpainless.PGPainless; import org.pgpainless.decryption_verification.ConsumerOptions; @@ -159,6 +160,8 @@ public class IgnoreMarkerPackets { } @Test + @Disabled + // TODO: Enable and fix public void markerPlusEncryptedMessage() throws IOException, PGPException { String msg = "-----BEGIN PGP MESSAGE-----\n" + "\n" + From 17f90eb721600c95a89b76deaa96edf87fb4c1e5 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 29 Sep 2022 13:14:32 +0200 Subject: [PATCH 20/80] Convert links in javadoc to html --- .../java/org/pgpainless/algorithm/PublicKeyAlgorithm.java | 6 +++--- .../decryption_verification/MissingPublicKeyCallback.java | 4 +++- .../key/util/PublicKeyParameterValidationUtil.java | 2 +- .../test/java/org/bouncycastle/PGPPublicKeyRingTest.java | 2 +- .../src/test/java/org/pgpainless/key/WeirdKeys.java | 2 +- .../key/generation/GenerateWithEmptyPassphraseTest.java | 2 +- 6 files changed, 10 insertions(+), 8 deletions(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/algorithm/PublicKeyAlgorithm.java b/pgpainless-core/src/main/java/org/pgpainless/algorithm/PublicKeyAlgorithm.java index baa8f92e..c7599db9 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/algorithm/PublicKeyAlgorithm.java +++ b/pgpainless-core/src/main/java/org/pgpainless/algorithm/PublicKeyAlgorithm.java @@ -28,7 +28,7 @@ public enum PublicKeyAlgorithm { /** * RSA with usage encryption. * - * @deprecated see https://tools.ietf.org/html/rfc4880#section-13.5 + * @deprecated see Deprecation notice */ @Deprecated RSA_ENCRYPT (PublicKeyAlgorithmTags.RSA_ENCRYPT, false, true), @@ -36,7 +36,7 @@ public enum PublicKeyAlgorithm { /** * RSA with usage of creating signatures. * - * @deprecated see https://tools.ietf.org/html/rfc4880#section-13.5 + * @deprecated see Deprecation notice */ @Deprecated RSA_SIGN (PublicKeyAlgorithmTags.RSA_SIGN, true, false), @@ -71,7 +71,7 @@ public enum PublicKeyAlgorithm { /** * ElGamal General. * - * @deprecated see https://tools.ietf.org/html/rfc4880#section-13.8 + * @deprecated see Deprecation notice */ @Deprecated ELGAMAL_GENERAL (PublicKeyAlgorithmTags.ELGAMAL_GENERAL, true, true), diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/MissingPublicKeyCallback.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/MissingPublicKeyCallback.java index f3eb949b..9b6f4a1e 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/MissingPublicKeyCallback.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/MissingPublicKeyCallback.java @@ -20,11 +20,13 @@ public interface MissingPublicKeyCallback { * you may not only search for the key-id on the key rings primary key! * * It would be super cool to provide the OpenPgp fingerprint here, but unfortunately one-pass-signatures - * only contain the key id (see https://datatracker.ietf.org/doc/html/rfc4880#section-5.4) + * only contain the key id. * * @param keyId ID of the missing signing (sub)key * * @return keyring containing the key or null + * + * @see RFC */ @Nullable PGPPublicKeyRing onMissingPublicKeyEncountered(@Nonnull Long keyId); diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/util/PublicKeyParameterValidationUtil.java b/pgpainless-core/src/main/java/org/pgpainless/key/util/PublicKeyParameterValidationUtil.java index d3c7fe68..69940691 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/key/util/PublicKeyParameterValidationUtil.java +++ b/pgpainless-core/src/main/java/org/pgpainless/key/util/PublicKeyParameterValidationUtil.java @@ -237,7 +237,7 @@ public class PublicKeyParameterValidationUtil { * Validate ElGamal public key parameters. * * Original implementation by the openpgpjs authors: - * https://github.com/openpgpjs/openpgpjs/blob/main/src/crypto/public_key/elgamal.js#L76-L143 + * Stackexchange link */ @Test public void subkeysDoNotHaveUserIDsTest() throws InvalidAlgorithmParameterException, NoSuchAlgorithmException, PGPException { diff --git a/pgpainless-core/src/test/java/org/pgpainless/key/WeirdKeys.java b/pgpainless-core/src/test/java/org/pgpainless/key/WeirdKeys.java index b02b07c5..7500bab4 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/key/WeirdKeys.java +++ b/pgpainless-core/src/test/java/org/pgpainless/key/WeirdKeys.java @@ -13,7 +13,7 @@ import org.pgpainless.PGPainless; * This class contains a set of slightly out of spec or weird keys. * Those are used to test whether implementations behave correctly when dealing with such keys. * - * Original source: https://gitlab.com/sequoia-pgp/weird-keys + * @see Original Source */ public class WeirdKeys { diff --git a/pgpainless-core/src/test/java/org/pgpainless/key/generation/GenerateWithEmptyPassphraseTest.java b/pgpainless-core/src/test/java/org/pgpainless/key/generation/GenerateWithEmptyPassphraseTest.java index dbb3f49b..c1375883 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/key/generation/GenerateWithEmptyPassphraseTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/key/generation/GenerateWithEmptyPassphraseTest.java @@ -20,7 +20,7 @@ import org.pgpainless.util.TestAllImplementations; import org.pgpainless.util.Passphrase; /** - * Reproduce behavior of https://github.com/pgpainless/pgpainless/issues/16 + * Reproduce behavior of Date: Thu, 29 Sep 2022 13:15:02 +0200 Subject: [PATCH 22/80] Reformat KeyRingReader --- .../pgpainless/key/parsing/KeyRingReader.java | 67 ++++++++++++------- 1 file changed, 44 insertions(+), 23 deletions(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/parsing/KeyRingReader.java b/pgpainless-core/src/main/java/org/pgpainless/key/parsing/KeyRingReader.java index bbaecd4f..50cacf05 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/key/parsing/KeyRingReader.java +++ b/pgpainless-core/src/main/java/org/pgpainless/key/parsing/KeyRingReader.java @@ -41,7 +41,8 @@ public class KeyRingReader { * @return key ring * @throws IOException in case of an IO error */ - public PGPKeyRing keyRing(@Nonnull InputStream inputStream) throws IOException { + public PGPKeyRing keyRing(@Nonnull InputStream inputStream) + throws IOException { return readKeyRing(inputStream); } @@ -53,7 +54,8 @@ public class KeyRingReader { * @return key ring * @throws IOException in case of an IO error */ - public PGPKeyRing keyRing(@Nonnull byte[] bytes) throws IOException { + public PGPKeyRing keyRing(@Nonnull byte[] bytes) + throws IOException { return keyRing(new ByteArrayInputStream(bytes)); } @@ -65,19 +67,23 @@ public class KeyRingReader { * @return key ring * @throws IOException in case of an IO error */ - public PGPKeyRing keyRing(@Nonnull String asciiArmored) throws IOException { + public PGPKeyRing keyRing(@Nonnull String asciiArmored) + throws IOException { return keyRing(asciiArmored.getBytes(UTF8)); } - public PGPPublicKeyRing publicKeyRing(@Nonnull InputStream inputStream) throws IOException { + public PGPPublicKeyRing publicKeyRing(@Nonnull InputStream inputStream) + throws IOException { return readPublicKeyRing(inputStream); } - public PGPPublicKeyRing publicKeyRing(@Nonnull byte[] bytes) throws IOException { + public PGPPublicKeyRing publicKeyRing(@Nonnull byte[] bytes) + throws IOException { return publicKeyRing(new ByteArrayInputStream(bytes)); } - public PGPPublicKeyRing publicKeyRing(@Nonnull String asciiArmored) throws IOException { + public PGPPublicKeyRing publicKeyRing(@Nonnull String asciiArmored) + throws IOException { return publicKeyRing(asciiArmored.getBytes(UTF8)); } @@ -86,23 +92,28 @@ public class KeyRingReader { return readPublicKeyRingCollection(inputStream); } - public PGPPublicKeyRingCollection publicKeyRingCollection(@Nonnull byte[] bytes) throws IOException, PGPException { + public PGPPublicKeyRingCollection publicKeyRingCollection(@Nonnull byte[] bytes) + throws IOException, PGPException { return publicKeyRingCollection(new ByteArrayInputStream(bytes)); } - public PGPPublicKeyRingCollection publicKeyRingCollection(@Nonnull String asciiArmored) throws IOException, PGPException { + public PGPPublicKeyRingCollection publicKeyRingCollection(@Nonnull String asciiArmored) + throws IOException, PGPException { return publicKeyRingCollection(asciiArmored.getBytes(UTF8)); } - public PGPSecretKeyRing secretKeyRing(@Nonnull InputStream inputStream) throws IOException { + public PGPSecretKeyRing secretKeyRing(@Nonnull InputStream inputStream) + throws IOException { return readSecretKeyRing(inputStream); } - public PGPSecretKeyRing secretKeyRing(@Nonnull byte[] bytes) throws IOException { + public PGPSecretKeyRing secretKeyRing(@Nonnull byte[] bytes) + throws IOException { return secretKeyRing(new ByteArrayInputStream(bytes)); } - public PGPSecretKeyRing secretKeyRing(@Nonnull String asciiArmored) throws IOException { + public PGPSecretKeyRing secretKeyRing(@Nonnull String asciiArmored) + throws IOException { return secretKeyRing(asciiArmored.getBytes(UTF8)); } @@ -111,11 +122,13 @@ public class KeyRingReader { return readSecretKeyRingCollection(inputStream); } - public PGPSecretKeyRingCollection secretKeyRingCollection(@Nonnull byte[] bytes) throws IOException, PGPException { + public PGPSecretKeyRingCollection secretKeyRingCollection(@Nonnull byte[] bytes) + throws IOException, PGPException { return secretKeyRingCollection(new ByteArrayInputStream(bytes)); } - public PGPSecretKeyRingCollection secretKeyRingCollection(@Nonnull String asciiArmored) throws IOException, PGPException { + public PGPSecretKeyRingCollection secretKeyRingCollection(@Nonnull String asciiArmored) + throws IOException, PGPException { return secretKeyRingCollection(asciiArmored.getBytes(UTF8)); } @@ -124,11 +137,13 @@ public class KeyRingReader { return readKeyRingCollection(inputStream, isSilent); } - public PGPKeyRingCollection keyRingCollection(@Nonnull byte[] bytes, boolean isSilent) throws IOException, PGPException { + public PGPKeyRingCollection keyRingCollection(@Nonnull byte[] bytes, boolean isSilent) + throws IOException, PGPException { return keyRingCollection(new ByteArrayInputStream(bytes), isSilent); } - public PGPKeyRingCollection keyRingCollection(@Nonnull String asciiArmored, boolean isSilent) throws IOException, PGPException { + public PGPKeyRingCollection keyRingCollection(@Nonnull String asciiArmored, boolean isSilent) + throws IOException, PGPException { return keyRingCollection(asciiArmored.getBytes(UTF8), isSilent); } @@ -142,7 +157,8 @@ public class KeyRingReader { * @return key ring * @throws IOException in case of an IO error */ - public static PGPKeyRing readKeyRing(@Nonnull InputStream inputStream) throws IOException { + public static PGPKeyRing readKeyRing(@Nonnull InputStream inputStream) + throws IOException { return readKeyRing(inputStream, MAX_ITERATIONS); } @@ -157,7 +173,8 @@ public class KeyRingReader { * @return key ring * @throws IOException in case of an IO error */ - public static PGPKeyRing readKeyRing(@Nonnull InputStream inputStream, int maxIterations) throws IOException { + public static PGPKeyRing readKeyRing(@Nonnull InputStream inputStream, int maxIterations) + throws IOException { PGPObjectFactory objectFactory = ImplementationFactory.getInstance().getPGPObjectFactory( ArmorUtils.getDecoderStream(inputStream)); int i = 0; @@ -181,7 +198,8 @@ public class KeyRingReader { throw new IOException("Loop exceeded max iteration count."); } - public static PGPPublicKeyRing readPublicKeyRing(@Nonnull InputStream inputStream) throws IOException { + public static PGPPublicKeyRing readPublicKeyRing(@Nonnull InputStream inputStream) + throws IOException { return readPublicKeyRing(inputStream, MAX_ITERATIONS); } @@ -196,7 +214,8 @@ public class KeyRingReader { * * @throws IOException in case of an IO error or exceeding of max iterations */ - public static PGPPublicKeyRing readPublicKeyRing(@Nonnull InputStream inputStream, int maxIterations) throws IOException { + public static PGPPublicKeyRing readPublicKeyRing(@Nonnull InputStream inputStream, int maxIterations) + throws IOException { PGPObjectFactory objectFactory = ImplementationFactory.getInstance().getPGPObjectFactory( ArmorUtils.getDecoderStream(inputStream)); int i = 0; @@ -218,7 +237,7 @@ public class KeyRingReader { } public static PGPPublicKeyRingCollection readPublicKeyRingCollection(@Nonnull InputStream inputStream) - throws IOException, PGPException { + throws IOException { return readPublicKeyRingCollection(inputStream, MAX_ITERATIONS); } @@ -264,7 +283,8 @@ public class KeyRingReader { throw new IOException("Loop exceeded max iteration count."); } - public static PGPSecretKeyRing readSecretKeyRing(@Nonnull InputStream inputStream) throws IOException { + public static PGPSecretKeyRing readSecretKeyRing(@Nonnull InputStream inputStream) + throws IOException { return readSecretKeyRing(inputStream, MAX_ITERATIONS); } @@ -279,7 +299,8 @@ public class KeyRingReader { * * @throws IOException in case of an IO error or exceeding of max iterations */ - public static PGPSecretKeyRing readSecretKeyRing(@Nonnull InputStream inputStream, int maxIterations) throws IOException { + public static PGPSecretKeyRing readSecretKeyRing(@Nonnull InputStream inputStream, int maxIterations) + throws IOException { InputStream decoderStream = ArmorUtils.getDecoderStream(inputStream); PGPObjectFactory objectFactory = ImplementationFactory.getInstance().getPGPObjectFactory(decoderStream); int i = 0; @@ -302,7 +323,7 @@ public class KeyRingReader { } public static PGPSecretKeyRingCollection readSecretKeyRingCollection(@Nonnull InputStream inputStream) - throws IOException, PGPException { + throws IOException { return readSecretKeyRingCollection(inputStream, MAX_ITERATIONS); } From 37dc362cc39aa2c3810a2786947638000d8ca020 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 29 Sep 2022 17:45:32 +0200 Subject: [PATCH 23/80] WIP: So close to working notarizations --- .../OpenPgpMessageInputStream.java | 359 +++++++++++------- .../TeeBCPGInputStream.java | 1 + .../automaton/PDA.java | 16 +- .../OpenPgpMessageInputStreamTest.java | 145 +++++++ version.gradle | 2 +- 5 files changed, 375 insertions(+), 148 deletions(-) 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 a1364997..2d031edc 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 @@ -4,12 +4,17 @@ package org.pgpainless.decryption_verification; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.NoSuchElementException; +import java.util.Stack; + import org.bouncycastle.bcpg.BCPGInputStream; -import org.bouncycastle.bcpg.BCPGOutputStream; -import org.bouncycastle.bcpg.OnePassSignaturePacket; -import org.bouncycastle.bcpg.Packet; -import org.bouncycastle.bcpg.PacketTags; -import org.bouncycastle.bcpg.SignaturePacket; import org.bouncycastle.openpgp.PGPCompressedData; import org.bouncycastle.openpgp.PGPEncryptedData; import org.bouncycastle.openpgp.PGPEncryptedDataList; @@ -17,7 +22,6 @@ import org.bouncycastle.openpgp.PGPException; import org.bouncycastle.openpgp.PGPLiteralData; import org.bouncycastle.openpgp.PGPObjectFactory; import org.bouncycastle.openpgp.PGPOnePassSignature; -import org.bouncycastle.openpgp.PGPOnePassSignatureList; import org.bouncycastle.openpgp.PGPPBEEncryptedData; import org.bouncycastle.openpgp.PGPPrivateKey; import org.bouncycastle.openpgp.PGPPublicKey; @@ -26,11 +30,12 @@ import org.bouncycastle.openpgp.PGPPublicKeyRing; import org.bouncycastle.openpgp.PGPSecretKey; import org.bouncycastle.openpgp.PGPSecretKeyRing; 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; import org.bouncycastle.openpgp.operator.PublicKeyDataDecryptorFactory; import org.bouncycastle.openpgp.operator.SessionKeyDataDecryptorFactory; +import org.bouncycastle.util.encoders.Hex; import org.pgpainless.PGPainless; import org.pgpainless.algorithm.CompressionAlgorithm; import org.pgpainless.algorithm.EncryptionPurpose; @@ -54,15 +59,6 @@ import org.pgpainless.util.Tuple; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.util.ArrayList; -import java.util.Collection; -import java.util.List; -import java.util.Stack; - public class OpenPgpMessageInputStream extends InputStream { private static final Logger LOGGER = LoggerFactory.getLogger(OpenPgpMessageInputStream.class); @@ -100,13 +96,7 @@ public class OpenPgpMessageInputStream extends InputStream { this.signatures.addDetachedSignatures(options.getDetachedSignatures()); } - // TODO: Use BCPGInputStream.wrap(inputStream); - BCPGInputStream bcpg = null; - if (inputStream instanceof BCPGInputStream) { - bcpg = (BCPGInputStream) inputStream; - } else { - bcpg = new BCPGInputStream(inputStream); - } + BCPGInputStream bcpg = BCPGInputStream.wrap(inputStream); this.packetInputStream = new TeeBCPGInputStream(bcpg, signatures); // *omnomnom* @@ -133,13 +123,20 @@ public class OpenPgpMessageInputStream extends InputStream { throws IOException, PGPException { int tag; loop: while ((tag = nextTag()) != -1) { - OpenPgpPacket nextPacket = OpenPgpPacket.requireFromTag(tag); + OpenPgpPacket nextPacket; + try { + nextPacket = OpenPgpPacket.requireFromTag(tag); + } catch (NoSuchElementException e) { + log("Invalid tag: " + tag); + throw e; + } + log(nextPacket.toString()); + signatures.nextPacket(nextPacket); switch (nextPacket) { // Literal Data - the literal data content is the new input stream case LIT: automaton.next(InputAlphabet.LiteralData); - signatures.commitNested(); processLiteralData(); break loop; @@ -153,12 +150,8 @@ public class OpenPgpMessageInputStream extends InputStream { // One Pass Signature case OPS: automaton.next(InputAlphabet.OnePassSignatures); - // signatures.addOnePassSignature(readOnePassSignature()); - PGPOnePassSignatureList onePassSignatureList = readOnePassSignatures(); - for (PGPOnePassSignature ops : onePassSignatureList) { - signatures.addOnePassSignature(ops); - } - // signatures.addOnePassSignatures(readOnePassSignatures()); + PGPOnePassSignature onePassSignature = readOnePassSignature(); + signatures.addOnePassSignature(onePassSignature); break; // Signature - either prepended to the message, or corresponding to a One Pass Signature @@ -166,13 +159,8 @@ public class OpenPgpMessageInputStream extends InputStream { boolean isSigForOPS = automaton.peekStack() == StackAlphabet.ops; automaton.next(InputAlphabet.Signatures); - // PGPSignature signature = readSignature(); - // processSignature(signature, isSigForOPS); - - PGPSignatureList signatureList = readSignatures(); - for (PGPSignature signature : signatureList) { - processSignature(signature, isSigForOPS); - } + PGPSignature signature = readSignature(); + processSignature(signature, isSigForOPS); break; @@ -220,6 +208,7 @@ public class OpenPgpMessageInputStream extends InputStream { private void processSignature(PGPSignature signature, boolean isSigForOPS) { if (isSigForOPS) { + signatures.popNested(); signatures.addCorrespondingOnePassSignature(signature); } else { signatures.addPrependedSignature(signature); @@ -240,6 +229,41 @@ public class OpenPgpMessageInputStream extends InputStream { nestedInputStream = literalData.getDataStream(); } + private void debugEncryptedData() throws PGPException, IOException { + PGPEncryptedDataList encDataList = new PGPEncryptedDataList(packetInputStream); + + // TODO: Replace with !encDataList.isIntegrityProtected() + if (!encDataList.get(0).isIntegrityProtected()) { + throw new MessageNotIntegrityProtectedException(); + } + + SortedESKs esks = new SortedESKs(encDataList); + for (PGPPublicKeyEncryptedData pkesk : esks.pkesks) { + long keyId = pkesk.getKeyID(); + PGPSecretKeyRing decryptionKeys = getDecryptionKey(keyId); + if (decryptionKeys == null) { + continue; + } + SecretKeyRingProtector protector = options.getSecretKeyProtector(decryptionKeys); + PGPSecretKey decryptionKey = decryptionKeys.getSecretKey(keyId); + PGPPrivateKey privateKey = UnlockSecretKey.unlockSecretKey(decryptionKey, protector); + + PublicKeyDataDecryptorFactory decryptorFactory = ImplementationFactory.getInstance() + .getPublicKeyDataDecryptorFactory(privateKey); + try { + InputStream decrypted = pkesk.getDataStream(decryptorFactory); + InputStream decoder = PGPUtil.getDecoderStream(decrypted); + PGPObjectFactory objectFactory = ImplementationFactory.getInstance() + .getPGPObjectFactory(decoder); + objectFactory.nextObject(); + objectFactory.nextObject(); + objectFactory.nextObject(); + } catch (PGPException e) { + // hm :/ + } + } + } + private boolean processEncryptedData() throws IOException, PGPException { PGPEncryptedDataList encDataList = new PGPEncryptedDataList(packetInputStream); @@ -309,7 +333,8 @@ public class OpenPgpMessageInputStream extends InputStream { InputStream decrypted = pkesk.getDataStream(decryptorFactory); MessageMetadata.EncryptedData encryptedData = new MessageMetadata.EncryptedData( SymmetricKeyAlgorithm.requireFromId(pkesk.getSymmetricAlgorithm(decryptorFactory))); - nestedInputStream = new OpenPgpMessageInputStream(decrypted, options, encryptedData); + + nestedInputStream = new OpenPgpMessageInputStream(PGPUtil.getDecoderStream(decrypted), options, encryptedData); return true; } catch (PGPException e) { // hm :/ @@ -382,52 +407,12 @@ public class OpenPgpMessageInputStream extends InputStream { private PGPOnePassSignature readOnePassSignature() throws PGPException, IOException { - //return new PGPOnePassSignature(packetInputStream); - return null; + return new PGPOnePassSignature(packetInputStream); } private PGPSignature readSignature() throws PGPException, IOException { - //return new PGPSignature(packetInputStream); - return null; - } - - private PGPOnePassSignatureList readOnePassSignatures() throws IOException { - ByteArrayOutputStream buf = new ByteArrayOutputStream(); - BCPGOutputStream bcpgOut = new BCPGOutputStream(buf); - int tag; - while ((tag = nextTag()) == PacketTags.ONE_PASS_SIGNATURE || tag == PacketTags.MARKER) { - Packet packet = packetInputStream.readPacket(); - if (tag == PacketTags.ONE_PASS_SIGNATURE) { - OnePassSignaturePacket sigPacket = (OnePassSignaturePacket) packet; - byte[] bytes = sigPacket.getEncoded(); - bcpgOut.write(bytes); - } - } - bcpgOut.close(); - - PGPObjectFactory objectFactory = ImplementationFactory.getInstance().getPGPObjectFactory(buf.toByteArray()); - PGPOnePassSignatureList signatureList = (PGPOnePassSignatureList) objectFactory.nextObject(); - return signatureList; - } - - private PGPSignatureList readSignatures() throws IOException { - ByteArrayOutputStream buf = new ByteArrayOutputStream(); - BCPGOutputStream bcpgOut = new BCPGOutputStream(buf); - int tag = nextTag(); - while (tag == PacketTags.SIGNATURE || tag == PacketTags.MARKER) { - Packet packet = packetInputStream.readPacket(); - if (tag == PacketTags.SIGNATURE) { - SignaturePacket sigPacket = (SignaturePacket) packet; - sigPacket.encode(bcpgOut); - tag = nextTag(); - } - } - bcpgOut.close(); - - PGPObjectFactory objectFactory = ImplementationFactory.getInstance().getPGPObjectFactory(buf.toByteArray()); - PGPSignatureList signatureList = (PGPSignatureList) objectFactory.nextObject(); - return signatureList; + return new PGPSignature(packetInputStream); } @Override @@ -446,12 +431,11 @@ public class OpenPgpMessageInputStream extends InputStream { boolean eos = r == -1; if (!eos) { byte b = (byte) r; - signatures.update(b); + signatures.updateLiteral(b); } else { nestedInputStream.close(); collectMetadata(); nestedInputStream = null; - signatures.popNested(); try { consumePackets(); @@ -473,11 +457,13 @@ public class OpenPgpMessageInputStream extends InputStream { } int r = nestedInputStream.read(b, off, len); - if (r == -1) { + if (r != -1) { + signatures.updateLiteral(b, off, r); + } + else { nestedInputStream.close(); collectMetadata(); nestedInputStream = null; - signatures.popNested(); try { consumePackets(); @@ -569,14 +555,11 @@ public class OpenPgpMessageInputStream extends InputStream { final ConsumerOptions options; final List detachedSignatures; final List prependedSignatures; - final List onePassSignatures; - final Stack> opsUpdateStack; + final List onePassSignatures; + final Stack> opsUpdateStack; + List literalOPS = new ArrayList<>(); final List correspondingSignatures; - ByteArrayOutputStream out = new ByteArrayOutputStream(); - - List opsCurrentNesting = new ArrayList<>(); - private Signatures(ConsumerOptions options) { this.options = options; this.detachedSignatures = new ArrayList<>(); @@ -608,57 +591,42 @@ public class OpenPgpMessageInputStream extends InputStream { void addOnePassSignature(PGPOnePassSignature signature) { PGPPublicKeyRing certificate = findCertificate(signature.getKeyID()); - initialize(signature, certificate); - onePassSignatures.add(signature); + OPS ops = new OPS(signature); + ops.init(certificate); + onePassSignatures.add(ops); - opsCurrentNesting.add(signature); - if (isContaining(signature)) { + literalOPS.add(ops); + if (signature.isContaining()) { commitNested(); } } - boolean isContaining(PGPOnePassSignature ops) { - try { - byte[] bytes = ops.getEncoded(); - return bytes[bytes.length - 1] == 1; - } catch (IOException e) { - return false; - } - } - void addCorrespondingOnePassSignature(PGPSignature signature) { - for (PGPOnePassSignature onePassSignature : onePassSignatures) { - if (onePassSignature.getKeyID() != signature.getKeyID()) { + for (int i = onePassSignatures.size() - 1; i >= 0; i--) { + OPS onePassSignature = onePassSignatures.get(i); + if (onePassSignature.signature.getKeyID() != signature.getKeyID()) { + continue; + } + if (onePassSignature.finished) { continue; } - boolean verified = false; - try { - verified = onePassSignature.verify(signature); - } catch (PGPException e) { - log("Cannot verify OPS signature.", e); - } - log("One-Pass-Signature by " + Long.toHexString(onePassSignature.getKeyID()) + " is " + (verified ? "verified" : "unverified")); - try { - log(ArmorUtils.toAsciiArmoredString(out.toByteArray())); - } catch (IOException e) { - throw new RuntimeException(e); - } + boolean verified = onePassSignature.verify(signature); + log("One-Pass-Signature by " + Long.toHexString(onePassSignature.signature.getKeyID()) + " is " + (verified ? "verified" : "unverified")); + System.out.println(onePassSignature); + break; } } void commitNested() { - if (opsCurrentNesting.isEmpty()) { - return; - } - - log("Committing " + opsCurrentNesting.size() + " OPS sigs for updating"); - opsUpdateStack.push(opsCurrentNesting); - opsCurrentNesting = new ArrayList<>(); + opsUpdateStack.push(literalOPS); + literalOPS = new ArrayList<>(); } void popNested() { - log("Popping nested"); + if (opsUpdateStack.isEmpty()) { + return; + } opsUpdateStack.pop(); } @@ -676,7 +644,7 @@ public class OpenPgpMessageInputStream extends InputStream { } } - private void initialize(PGPOnePassSignature ops, PGPPublicKeyRing certificate) { + private static void initialize(PGPOnePassSignature ops, PGPPublicKeyRing certificate) { if (certificate == null) { return; } @@ -699,20 +667,9 @@ public class OpenPgpMessageInputStream extends InputStream { return null; // TODO: Missing cert for sig } - public void update(byte b) { - if (!opsUpdateStack.isEmpty()) { - log("Update"); - out.write(b); - } - - for (PGPSignature prepended : prependedSignatures) { - prepended.update(b); - } - - for (List opss : opsUpdateStack) { - for (PGPOnePassSignature ops : opss) { - ops.update(b); - } + public void updateLiteral(byte b) { + for (OPS ops : literalOPS) { + ops.update(b); } for (PGPSignature detached : detachedSignatures) { @@ -720,6 +677,33 @@ public class OpenPgpMessageInputStream extends InputStream { } } + public void updateLiteral(byte[] b, int off, int len) { + for (OPS ops : literalOPS) { + ops.update(b, off, len); + } + + for (PGPSignature detached : detachedSignatures) { + detached.update(b, off, len); + } + } + + public void updatePacket(byte b) { + for (List nestedOPSs : opsUpdateStack) { + for (OPS ops : nestedOPSs) { + ops.update(b); + } + } + } + + public void updatePacket(byte[] buf, int off, int len) { + for (int i = opsUpdateStack.size() - 1; i >= 0; i--) { + List nestedOPSs = opsUpdateStack.get(i); + for (OPS ops : nestedOPSs) { + ops.update(buf, off, len); + } + } + } + public void finish() { for (PGPSignature detached : detachedSignatures) { boolean verified = false; @@ -743,8 +727,97 @@ public class OpenPgpMessageInputStream extends InputStream { } @Override - public void write(int b) throws IOException { - update((byte) b); + public void write(int b) { + updatePacket((byte) b); + } + + @Override + public void write(byte[] b, int off, int len) { + updatePacket(b, off, len); + } + + public void nextPacket(OpenPgpPacket nextPacket) { + if (nextPacket == OpenPgpPacket.LIT) { + if (literalOPS.isEmpty() && !opsUpdateStack.isEmpty()) { + literalOPS = opsUpdateStack.pop(); + } + } + } + + static class OPS { + ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + PGPOnePassSignature signature; + boolean finished; + boolean valid; + + public OPS(PGPOnePassSignature signature) { + this.signature = signature; + } + + public void init(PGPPublicKeyRing certificate) { + initialize(signature, certificate); + } + + public boolean verify(PGPSignature signature) { + if (this.signature.getKeyID() != signature.getKeyID()) { + // nope + return false; + } + finished = true; + try { + valid = this.signature.verify(signature); + } catch (PGPException e) { + log("Cannot verify OPS " + signature.getKeyID()); + } + return valid; + } + + public void update(byte b) { + if (finished) { + log("Updating finished sig!"); + return; + } + signature.update(b); + bytes.write(b); + } + + public void update(byte[] bytes, int off, int len) { + if (finished) { + log("Updating finished sig!"); + return; + } + signature.update(bytes, off, len); + this.bytes.write(bytes, off, len); + } + + @Override + public String toString() { + String OPS = "c40d03000a01fbfcc82a015e733001"; + String LIT_H = "cb28620000000000"; + String LIT = "656e637279707420e28898207369676e20e28898207369676e20e28898207369676e"; + String SIG1 = "c2c10400010a006f058262c806350910fbfcc82a015e7330471400000000001e002073616c74406e6f746174696f6e732e736571756f69612d7067702e6f7267b0409ed8ea96dac66447bdff5b7b60c9f80a0ab91d257029153dc3b6d8c27b98162104d1a66e1a23b182c9980f788cfbfcc82a015e7330000029640c00846b5096d92474fd446cc7edaf9f14572cab93a80e12384c1e829f95debc6e8373c2ce5402be53dc1a18cf92a0ed909e0fb38855713ef8ffb13502ffac7c830fa254cc1aa6c666a97b0cc3bc176538f6913d3b8e8981a65cc42df10e0f39e4d0a06dfe961437b59a71892f4fca1116aed15123ea0d86a7b2ce47dd9d3ef22d920631bc011e82babe03ad5d72b3ba7f95bf646f20ccf6f7a4d95de37397c76c7d53741458e51ab6074007f61181c7b88b7c98f5b7510c8dfa3be01f4841501679478b15c5249d928e2a10d15ec63efa1500b994d5bfb32ffb174a976116930eb97a111e6dfd4c5e43e04a5d76ba74806a62fda63a8c3f53f6eebaf852892340e81dd08bbf348454a2cf525aeb512cf33aeeee78465ee4c442e41cc45ac4e3bb0c3333677aa60332ee7f464d9020f8554b82d619872477cca18d8431888f4ae8abe5894e9720f759c410cd7991db12703dc147040dd0d3758223e0b75de6ceae49c1a0c2c45efedeb7114ae785cc886afdc45c82172e4476e1ab5b86dc4314dd76"; + String SIG2 = "c2c10400010a006f058262c806350910fbfcc82a015e7330471400000000001e002073616c74406e6f746174696f6e732e736571756f69612d7067702e6f7267a4d9c117dc7ba3a7e9270856f128d2ab271743eac3cb5750b22a89bd5fd60753162104d1a66e1a23b182c9980f788cfbfcc82a015e73300000b8400bff796c20fa8b25ff7a42686338e06417a2966e85a0fc2723c928bef6cd19d34cf5e7d55ada33080613012dadb79e0278e59d9e7ed7d2d6102912a5f768c2e75b60099225c3d8bfe0c123240188b80dbee89b9b3bd5b13ccc662abc37e2129b6968adac9aba43aa778c0fe4fe337591ee87a96a29a013debc83555293c877144fc676aa1b03782c501949521a320adf6ad96c4f2e036b52a18369c637fdc49033696a84d03a69580b953187fce5aca6fb26fc8815da9f3b513bfe8e304f33ecb4b521aeb7d09c4a284ea66123bd0d6a358b2526d762ca110e1f7f20b3038d774b64d5cfd34e2213765828359d7afc5bf24d5270e99d80c3c1568fa01624b6ea1e9ce4e6890ce9bacf6611a45d41e2671f68f5b096446bf08d27ce75608425b2e3ab92146229ad1fcd8224aca5b5f73960506e7df07bfbf3664348e8ecbfb2eb467b9cfe412cb377a6ee2eb5fd11be9cf9208fe9a74c296f52cfa02a1eb0519ad9a8349bf6ccd6495feb7e391451bf96e08a0798883dee5974e47cbf3b51f111b6d3"; + String out = signature.getKeyID() + " last=" + signature.isContaining() + "\n"; + + String hex = Hex.toHexString(bytes.toByteArray()); + while (hex.contains(OPS)) { + hex = hex.replace(OPS, "[OPS]"); + } + while (hex.contains(LIT_H)) { + hex = hex.replace(LIT_H, "[LIT]"); + } + while (hex.contains(LIT)) { + hex = hex.replace(LIT, ""); + } + while (hex.contains(SIG1)) { + hex = hex.replace(SIG1, "[SIG1]"); + } + while (hex.contains(SIG2)) { + hex = hex.replace(SIG2, "[SIG2]"); + } + + return out + hex; + } } } diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/TeeBCPGInputStream.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/TeeBCPGInputStream.java index ab24f22e..cce21051 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/TeeBCPGInputStream.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/TeeBCPGInputStream.java @@ -1,6 +1,7 @@ package org.pgpainless.decryption_verification; import org.bouncycastle.bcpg.BCPGInputStream; +import org.pgpainless.util.ArmorUtils; import java.io.IOException; import java.io.InputStream; 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 dda2adce..1d4b1189 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 @@ -149,11 +149,19 @@ public class PDA { @Override State transition(InputAlphabet input, PDA automaton) throws MalformedOpenPgpMessageException { StackAlphabet stackItem = automaton.popStack(); - if (stackItem == terminus && input == InputAlphabet.EndOfSequence && automaton.stack.isEmpty()) { - return Valid; - } else { - throw new MalformedOpenPgpMessageException(this, input, stackItem); + if (input == InputAlphabet.EndOfSequence) { + if (stackItem == terminus && automaton.stack.isEmpty()) { + return Valid; + } else { + // premature end of stream + throw new MalformedOpenPgpMessageException(this, input, stackItem); + } + } else if (input == InputAlphabet.Signatures) { + if (stackItem == ops) { + return CorrespondingSignature; + } } + throw new MalformedOpenPgpMessageException(this, input, stackItem); } }, 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 e52a6016..f780b2eb 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 @@ -433,6 +433,151 @@ public class OpenPgpMessageInputStreamTest { assertNull(metadata.getCompressionAlgorithm()); } + @ParameterizedTest(name = "Process PENC(OPS OPS OPS LIT SIG SIG SIG) using {0}") + @MethodSource("provideMessageProcessors") + public void testProcessOPS_OPS_OPS_LIT_SIG_SIG_SIG(Processor processor) throws IOException, PGPException { + String KEY = "-----BEGIN PGP PRIVATE KEY BLOCK-----\n" + + "Comment: Bob's OpenPGP Transferable Secret Key\n" + + "\n" + + "lQVYBF2lnPIBDAC5cL9PQoQLTMuhjbYvb4Ncuuo0bfmgPRFywX53jPhoFf4Zg6mv\n" + + "/seOXpgecTdOcVttfzC8ycIKrt3aQTiwOG/ctaR4Bk/t6ayNFfdUNxHWk4WCKzdz\n" + + "/56fW2O0F23qIRd8UUJp5IIlN4RDdRCtdhVQIAuzvp2oVy/LaS2kxQoKvph/5pQ/\n" + + "5whqsyroEWDJoSV0yOb25B/iwk/pLUFoyhDG9bj0kIzDxrEqW+7Ba8nocQlecMF3\n" + + "X5KMN5kp2zraLv9dlBBpWW43XktjcCZgMy20SouraVma8Je/ECwUWYUiAZxLIlMv\n" + + "9CurEOtxUw6N3RdOtLmYZS9uEnn5y1UkF88o8Nku890uk6BrewFzJyLAx5wRZ4F0\n" + + "qV/yq36UWQ0JB/AUGhHVPdFf6pl6eaxBwT5GXvbBUibtf8YI2og5RsgTWtXfU7eb\n" + + "SGXrl5ZMpbA6mbfhd0R8aPxWfmDWiIOhBufhMCvUHh1sApMKVZnvIff9/0Dca3wb\n" + + "vLIwa3T4CyshfT0AEQEAAQAL/RZqbJW2IqQDCnJi4Ozm++gPqBPiX1RhTWSjwxfM\n" + + "cJKUZfzLj414rMKm6Jh1cwwGY9jekROhB9WmwaaKT8HtcIgrZNAlYzANGRCM4TLK\n" + + "3VskxfSwKKna8l+s+mZglqbAjUg3wmFuf9Tj2xcUZYmyRm1DEmcN2ZzpvRtHgX7z\n" + + "Wn1mAKUlSDJZSQks0zjuMNbupcpyJokdlkUg2+wBznBOTKzgMxVNC9b2g5/tMPUs\n" + + "hGGWmF1UH+7AHMTaS6dlmr2ZBIyogdnfUqdNg5sZwsxSNrbglKP4sqe7X61uEAIQ\n" + + "bD7rT3LonLbhkrj3I8wilUD8usIwt5IecoHhd9HziqZjRCc1BUBkboUEoyedbDV4\n" + + "i4qfsFZ6CEWoLuD5pW7dEp0M+WeuHXO164Rc+LnH6i1VQrpb1Okl4qO6ejIpIjBI\n" + + "1t3GshtUu/mwGBBxs60KBX5g77mFQ9lLCRj8lSYqOsHRKBhUp4qM869VA+fD0BRP\n" + + "fqPT0I9IH4Oa/A3jYJcg622GwQYA1LhnP208Waf6PkQSJ6kyr8ymY1yVh9VBE/g6\n" + + "fRDYA+pkqKnw9wfH2Qho3ysAA+OmVOX8Hldg+Pc0Zs0e5pCavb0En8iFLvTA0Q2E\n" + + "LR5rLue9uD7aFuKFU/VdcddY9Ww/vo4k5p/tVGp7F8RYCFn9rSjIWbfvvZi1q5Tx\n" + + "+akoZbga+4qQ4WYzB/obdX6SCmi6BndcQ1QdjCCQU6gpYx0MddVERbIp9+2SXDyL\n" + + "hpxjSyz+RGsZi/9UAshT4txP4+MZBgDfK3ZqtW+h2/eMRxkANqOJpxSjMyLO/FXN\n" + + "WxzTDYeWtHNYiAlOwlQZEPOydZFty9IVzzNFQCIUCGjQ/nNyhw7adSgUk3+BXEx/\n" + + "MyJPYY0BYuhLxLYcrfQ9nrhaVKxRJj25SVHj2ASsiwGJRZW4CC3uw40OYxfKEvNC\n" + + "mer/VxM3kg8qqGf9KUzJ1dVdAvjyx2Hz6jY2qWCyRQ6IMjWHyd43C4r3jxooYKUC\n" + + "YnstRQyb/gCSKahveSEjo07CiXMr88UGALwzEr3npFAsPW3osGaFLj49y1oRe11E\n" + + "he9gCHFm+fuzbXrWmdPjYU5/ZdqdojzDqfu4ThfnipknpVUM1o6MQqkjM896FHm8\n" + + "zbKVFSMhEP6DPHSCexMFrrSgN03PdwHTO6iBaIBBFqmGY01tmJ03SxvSpiBPON9P\n" + + "NVvy/6UZFedTq8A07OUAxO62YUSNtT5pmK2vzs3SAZJmbFbMh+NN204TRI72GlqT\n" + + "t5hcfkuv8hrmwPS/ZR6q312mKQ6w/1pqO9qitCFCb2IgQmFiYmFnZSA8Ym9iQG9w\n" + + "ZW5wZ3AuZXhhbXBsZT6JAc4EEwEKADgCGwMFCwkIBwIGFQoJCAsCBBYCAwECHgEC\n" + + "F4AWIQTRpm4aI7GCyZgPeIz7/MgqAV5zMAUCXaWe+gAKCRD7/MgqAV5zMG9sC/9U\n" + + "2T3RrqEbw533FPNfEflhEVRIZ8gDXKM8hU6cqqEzCmzZT6xYTe6sv4y+PJBGXJFX\n" + + "yhj0g6FDkSyboM5litOcTupURObVqMgA/Y4UKERznm4fzzH9qek85c4ljtLyNufe\n" + + "doL2pp3vkGtn7eD0QFRaLLmnxPKQ/TlZKdLE1G3u8Uot8QHicaR6GnAdc5UXQJE3\n" + + "BiV7jZuDyWmZ1cUNwJkKL6oRtp+ZNDOQCrLNLecKHcgCqrpjSQG5oouba1I1Q6Vl\n" + + "sP44dhA1nkmLHtxlTOzpeHj4jnk1FaXmyasurrrI5CgU/L2Oi39DGKTH/A/cywDN\n" + + "4ZplIQ9zR8enkbXquUZvFDe+Xz+6xRXtb5MwQyWODB3nHw85HocLwRoIN9WdQEI+\n" + + "L8a/56AuOwhs8llkSuiITjR7r9SgKJC2WlAHl7E8lhJ3VDW3ELC56KH308d6mwOG\n" + + "ZRAqIAKzM1T5FGjMBhq7ZV0eqdEntBh3EcOIfj2M8rg1MzJv+0mHZOIjByawikad\n" + + "BVgEXaWc8gEMANYwv1xsYyunXYK0X1vY/rP1NNPvhLyLIE7NpK90YNBj+xS1ldGD\n" + + "bUdZqZeef2xJe8gMQg05DoD1DF3GipZ0Ies65beh+d5hegb7N4pzh0LzrBrVNHar\n" + + "29b5ExdI7i4iYD5TO6Vr/qTUOiAN/byqELEzAb+L+b2DVz/RoCm4PIp1DU9ewcc2\n" + + "WB38Ofqut3nLYA5tqJ9XvAiEQme+qAVcM3ZFcaMt4I4dXhDZZNg+D9LiTWcxdUPB\n" + + "leu8iwDRjAgyAhPzpFp+nWoqWA81uIiULWD1Fj+IVoY3ZvgivoYOiEFBJ9lbb4te\n" + + "g9m5UT/AaVDTWuHzbspVlbiVe+qyB77C2daWzNyx6UYBPLOo4r0t0c91kbNE5lgj\n" + + "Z7xz6los0N1U8vq91EFSeQJoSQ62XWavYmlCLmdNT6BNfgh4icLsT7Vr1QMX9jzn\n" + + "JtTPxdXytSdHvpSpULsqJ016l0dtmONcK3z9mj5N5z0k1tg1AH970TGYOe2aUcSx\n" + + "IRDMXDOPyzEfjwARAQABAAv9F2CwsjS+Sjh1M1vegJbZjei4gF1HHpEM0K0PSXsp\n" + + "SfVvpR4AoSJ4He6CXSMWg0ot8XKtDuZoV9jnJaES5UL9pMAD7JwIOqZm/DYVJM5h\n" + + "OASCh1c356/wSbFbzRHPtUdZO9Q30WFNJM5pHbCJPjtNoRmRGkf71RxtvHBzy7np\n" + + "Ga+W6U/NVKHw0i0CYwMI0YlKDakYW3Pm+QL+gHZFvngGweTod0f9l2VLLAmeQR/c\n" + + "+EZs7lNumhuZ8mXcwhUc9JQIhOkpO+wreDysEFkAcsKbkQP3UDUsA1gFx9pbMzT0\n" + + "tr1oZq2a4QBtxShHzP/ph7KLpN+6qtjks3xB/yjTgaGmtrwM8tSe0wD1RwXS+/1o\n" + + "BHpXTnQ7TfeOGUAu4KCoOQLv6ELpKWbRBLWuiPwMdbGpvVFALO8+kvKAg9/r+/ny\n" + + "zM2GQHY+J3Jh5JxPiJnHfXNZjIKLbFbIPdSKNyJBuazXW8xIa//mEHMI5OcvsZBK\n" + + "clAIp7LXzjEjKXIwHwDcTn9pBgDpdOKTHOtJ3JUKx0rWVsDH6wq6iKV/FTVSY5jl\n" + + "zN+puOEsskF1Lfxn9JsJihAVO3yNsp6RvkKtyNlFazaCVKtDAmkjoh60XNxcNRqr\n" + + "gCnwdpbgdHP6v/hvZY54ZaJjz6L2e8unNEkYLxDt8cmAyGPgH2XgL7giHIp9jrsQ\n" + + "aS381gnYwNX6wE1aEikgtY91nqJjwPlibF9avSyYQoMtEqM/1UjTjB2KdD/MitK5\n" + + "fP0VpvuXpNYZedmyq4UOMwdkiNMGAOrfmOeT0olgLrTMT5H97Cn3Yxbk13uXHNu/\n" + + "ZUZZNe8s+QtuLfUlKAJtLEUutN33TlWQY522FV0m17S+b80xJib3yZVJteVurrh5\n" + + "HSWHAM+zghQAvCesg5CLXa2dNMkTCmZKgCBvfDLZuZbjFwnwCI6u/NhOY9egKuUf\n" + + "SA/je/RXaT8m5VxLYMxwqQXKApzD87fv0tLPlVIEvjEsaf992tFEFSNPcG1l/jpd\n" + + "5AVXw6kKuf85UkJtYR1x2MkQDrqY1QX/XMw00kt8y9kMZUre19aCArcmor+hDhRJ\n" + + "E3Gt4QJrD9z/bICESw4b4z2DbgD/Xz9IXsA/r9cKiM1h5QMtXvuhyfVeM01enhxM\n" + + "GbOH3gjqqGNKysx0UODGEwr6AV9hAd8RWXMchJLaExK9J5SRawSg671ObAU24SdY\n" + + "vMQ9Z4kAQ2+1ReUZzf3ogSMRZtMT+d18gT6L90/y+APZIaoArLPhebIAGq39HLmJ\n" + + "26x3z0WAgrpA1kNsjXEXkoiZGPLKIGoe3hqJAbYEGAEKACAWIQTRpm4aI7GCyZgP\n" + + "eIz7/MgqAV5zMAUCXaWc8gIbDAAKCRD7/MgqAV5zMOn/C/9ugt+HZIwX308zI+QX\n" + + "c5vDLReuzmJ3ieE0DMO/uNSC+K1XEioSIZP91HeZJ2kbT9nn9fuReuoff0T0Dief\n" + + "rbwcIQQHFFkrqSp1K3VWmUGp2JrUsXFVdjy/fkBIjTd7c5boWljv/6wAsSfiv2V0\n" + + "JSM8EFU6TYXxswGjFVfc6X97tJNeIrXL+mpSmPPqy2bztcCCHkWS5lNLWQw+R7Vg\n" + + "71Fe6yBSNVrqC2/imYG2J9zlowjx1XU63Wdgqp2Wxt0l8OmsB/W80S1fRF5G4SDH\n" + + "s9HXglXXqPsBRZJYfP+VStm9L5P/sKjCcX6WtZR7yS6G8zj/X767MLK/djANvpPd\n" + + "NVniEke6hM3CNBXYPAMhQBMWhCulcoz+0lxi8L34rMN+Dsbma96psdUrn7uLaB91\n" + + "6we0CTfF8qqm7BsVAgalon/UUiuMY80U3ueoj3okiSTiHIjD/YtpXSPioC8nMng7\n" + + "xqAY9Bwizt4FWgXuLm1a4+So4V9j1TRCXd12Uc2l2RNmgDE=\n" + + "=miES\n" + + "-----END PGP PRIVATE KEY BLOCK-----"; + String MSG = "-----BEGIN PGP MESSAGE-----\n" + + "\n" + + "wcDMA3wvqk35PDeyAQwA0yaEgydkAMEfl7rDTYVGanLKiFiWIs34mkF+LB8qR5eY\n" + + "ZRuhodPbX9QjpgOZ8fETPU3DEvzOaR0kMqKAHl7mmP0inydK5vpx0U0JTHIZkNeC\n" + + "rQbizphG2VA8fUvxZ79bZAe43uguITI2R2EZgEzeq6vCO7Ca4XqK95OADKZoVzS7\n" + + "0uBSMIgAVumrAj2l1ZOYbiIevx0+xJT2NvsLj7TV3ewBIyUg2f5NujcgEnuhpsMu\n" + + "wM/k58u4iBLAa8Qr2f8WFvLRwH3btfiT9VlKaW+JvIvU9RuNKhMihNY4PXV1uJfv\n" + + "kKsarMDlRgeRMUHJitwCQP3CSiT+ATCmfHz5e83qsJjBPC0d8qc1H+WKYZ2TPvWO\n" + + "egzFLTK73ruhTxGeotr4j6fldriewa/S8R9RHWu+6S3NJ9LNWnt9zUJ85d+f0wY3\n" + + "GVub3y20Zh1dm8A+hnNvK5EB5JyIEP8SFH2N9Cs2YQJn8X7aWYRuBq4KryQDb20n\n" + + "l4FAiRk414D2Z7XKDvxO0sW6AclnT0DfBm4jZDWquY8U5QsAOtvmMhHlZYVlGm8s\n" + + "caqoTx9xMugVzkdWv496nx9kFpMWaNB4KBi5B8MBXOeZchOEFIujH0jeWOXUWgJt\n" + + "hWfNMJSliYlS6VO9aM3ab5SAPcPiHmCkuXXtWBWtmUyUkbWCrZdgq7b4UfGiwQeI\n" + + "q584RnwPOnRpUfglalP1UqufbJMyl7CFjEMVkcxhApp/zgFZZj0w8oeh9aGflcYJ\n" + + "PDvsFoJV0P+VbHlI3FTIg+tJZ73gT/X54Mj5ifUpIZQ/abXSSsgrgnZ4qAjLf8Om\n" + + "GOly5ITEfxJC5rir1yLyBM4T8YJpra3A+3VJo7x/ZatiOxs40uBB4zILIjs5LlCe\n" + + "WAhFzGzq+VvV7LD6c03USxuV70LhfCUH6ZRq4iXFSnjOoWr5tvWZgzVAc7fshlad\n" + + "XZB6lz03jWgNvY66kJK5O6pJ8dftuyihHFY7e44+gQttb+41cYhDmm0Nxxq4PDKW\n" + + "CvI2ETpnW24792D+ZI7XMEfZhY2LoXGYvCkGt5aeo/dsWHoKa3yDjp5/rc2llEFz\n" + + "A3P8mznBfaRNVjW/UhpMAUI3/kn2bbw21ogrm0NuwZGWIS5ea7+G8TjbrznIQsTq\n" + + "VlLhMc7d6gK3hKdDsplX5J90YLA0l1SbQGHqb6GXOsIO2tSRpZWUQIIinYdMDmBG\n" + + "b1wPdwtXmCtyqJfGs/vwmoZdZ0FnwmcsF+bI7LSUnZMK/Cno/Tcl6kWJtvLtG2eC\n" + + "pHxD/tsU3DoArpDa/+/DOotq+u0CB6ymGAi/NnkFKUdNs8oEt0eOw27/F1teKSgv\n" + + "wF4KEcbrHoeSlk/95rtnJYT4IkNA1GSZgYALAMSO2sv7XeBab/jRqM7hyMmzKb3R\n" + + "uXN+BcDHRA1vdvIEpnTD5/EDon3/mr7xgHctzuK8z30aruQoBHWckIgmibB5LNvV\n" + + "xvFFPFkke6dxEXbYWwYwrqUSHk74420euGa58jnuXtQIr0X+g+UTJegzOjt96ZJH\n" + + "l92AHadooL7jYiPX8qxw1sln7k0H+RfWSvEbZ0/xsQ0lxgYwds/Ck6yhOUK8hyRW\n" + + "OVmz3g1QjdwZUDblypsymO3iFggJ0NNhNlYPKEWmwdfTOMDmtuQS97ewDSv0WgAa\n" + + "oUx2FjjM4iOKiyKsM5i8a4ju3MziFu1ghOfixBwtHRbQHneF5/E5cFtrYvuOlAvN\n" + + "80r89YesbBzXzsvheez+bIhm4lTHvBKgcb/RNaseYz/72HVk24GGnisSuc37v+O4\n" + + "YcLflfi86KuLtYQNtR+QyegfYWYogjbsSocWBEfnPJBgtzAtdAnMkaKWbb6WfT4k\n" + + "J6KWH/wANNdjE4yXPJhRevn3PqHnQvKHJqef1DZgzQMcXD3BwOPXxzy1GXXJw4Jn\n" + + "Ma1izl7a+KdbPonCnT59Kg24sl6gJplJRZop/tBqUR/c08kIuEuOB1D+qkeAIv6A\n" + + "3/uK7l4PvVe7XSjZ12Rfm2S7cY4dQybgW81TWKfCDNNXjSAWGAKtfIO7iojzBTF0\n" + + "MPfpuAx0sP++qUXZGsxIOKUhlqZpDNboHw89UDjj8txc9p6NbWTy6VJoYTKv07sG\n" + + "4Umrl5oaX49Ub0GlnwWg/wweCrMXszvZAN58qG0Qt2sjnHy1tUIJ7OajDpWrAEYt\n" + + "cvGzFvsr/j2k9lXBrgtIfSIWo8oQhXDR1gsBw5AxnCWkX0gQPEjYv+rq5zHxfWrF\n" + + "IOG3zXyoO8QHU0TwdA3s7XBd1pbtyaX0BksW7ecqa+J2KkbXhUOQwMTpgCIGkcBV\n" + + "CWf3w6voe6ZPfz4KPR3Zbs9ypV6nbfKjUjjfq7Lms1kOVJqZlJp5hf+ew6hxETHp\n" + + "0QmdhONHZvl+25z4rOquuBwsBXvFw/V5dlvuusi9VBuTUwh/v9JARSNmql8V054M\n" + + "o6Strj5Ukn+ejymZqXs9yeA+cgE3FL4hzdrUEUt8IVLxvD/XYuWROQJ7AckmU9GA\n" + + "xpQxbGcDMV6JzkDihKhiX3D6poccaaaFYv85NNCncsDJrPHrU48PQ4qOyr2sFQa+\n" + + "sfLYfRv5W60Zj3OyVFlK2JrqCu5sT7tecoxCGPCR0m/IpQYYu99JxN2SFv2vV9HI\n" + + "R6Vg18KxWerJ4sWGDe1CKeCCARiBGD8eNajf6JRu+K9VWUjmYpiEkK68Xaa4/Q2T\n" + + "x12WVuyITVU3fCfHp6/0A6wPtJezCvoodqPlw/3fd5eSVYzb5C3v564uhz4=\n" + + "=JP9T\n" + + "-----END PGP MESSAGE-----"; + PGPSecretKeyRing secretKeys = PGPainless.readKeyRing().secretKeyRing(KEY); + PGPPublicKeyRing certificate = PGPainless.extractCertificate(secretKeys); + + Tuple result = processor.process(MSG, ConsumerOptions.get() + .addVerificationCert(certificate) + .addDecryptionKey(secretKeys)); + String plain = result.getA(); + assertEquals("encrypt ∘ sign ∘ sign ∘ sign", plain); + MessageMetadata metadata = result.getB(); + assertEquals(SymmetricKeyAlgorithm.AES_256, metadata.getEncryptionAlgorithm()); + assertNull(metadata.getCompressionAlgorithm()); + } + private static Tuple processReadBuffered(String armoredMessage, ConsumerOptions options) throws PGPException, IOException { OpenPgpMessageInputStream in = get(armoredMessage, options); diff --git a/version.gradle b/version.gradle index 680b9d45..1069132c 100644 --- a/version.gradle +++ b/version.gradle @@ -4,7 +4,7 @@ allprojects { ext { - shortVersion = '1.3.9' + shortVersion = '1.4.0' isSnapshot = true pgpainlessMinAndroidSdk = 10 javaSourceCompatibility = 1.8 From c65e484bb451e965f21b5384cb8232160c88bc37 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 6 Oct 2022 21:52:23 +0200 Subject: [PATCH 24/80] 2/3 the way to working sig verification --- .../DelayedTeeInputStreamInputStream.java | 38 +++ .../OpenPgpMessageInputStream.java | 263 ++++++++++++------ .../TeeBCPGInputStream.java | 36 --- .../OpenPgpMessageInputStreamTest.java | 234 ++++++++++------ .../TeeBCPGInputStreamTest.java | 61 ---- 5 files changed, 353 insertions(+), 279 deletions(-) create mode 100644 pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DelayedTeeInputStreamInputStream.java delete mode 100644 pgpainless-core/src/main/java/org/pgpainless/decryption_verification/TeeBCPGInputStream.java delete mode 100644 pgpainless-core/src/test/java/org/pgpainless/decryption_verification/TeeBCPGInputStreamTest.java diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DelayedTeeInputStreamInputStream.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DelayedTeeInputStreamInputStream.java new file mode 100644 index 00000000..5cbabf89 --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DelayedTeeInputStreamInputStream.java @@ -0,0 +1,38 @@ +package org.pgpainless.decryption_verification; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +public class DelayedTeeInputStreamInputStream extends InputStream { + + private int last = -1; + private final InputStream inputStream; + private final OutputStream outputStream; + + public DelayedTeeInputStreamInputStream(InputStream inputStream, OutputStream outputStream) { + this.inputStream = inputStream; + this.outputStream = outputStream; + } + + @Override + public int read() throws IOException { + if (last != -1) { + outputStream.write(last); + } + last = inputStream.read(); + return last; + } + + /** + * Squeeze the last byte out and update the output stream. + * + * @throws IOException in case of an IO error + */ + public void squeeze() throws IOException { + if (last != -1) { + outputStream.write(last); + } + last = -1; + } +} 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 2d031edc..8d370965 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 @@ -20,7 +20,6 @@ import org.bouncycastle.openpgp.PGPEncryptedData; import org.bouncycastle.openpgp.PGPEncryptedDataList; import org.bouncycastle.openpgp.PGPException; import org.bouncycastle.openpgp.PGPLiteralData; -import org.bouncycastle.openpgp.PGPObjectFactory; import org.bouncycastle.openpgp.PGPOnePassSignature; import org.bouncycastle.openpgp.PGPPBEEncryptedData; import org.bouncycastle.openpgp.PGPPrivateKey; @@ -53,7 +52,6 @@ import org.pgpainless.key.info.KeyRingInfo; import org.pgpainless.key.protection.SecretKeyRingProtector; import org.pgpainless.key.protection.UnlockSecretKey; import org.pgpainless.signature.SignatureUtils; -import org.pgpainless.util.ArmorUtils; import org.pgpainless.util.Passphrase; import org.pgpainless.util.Tuple; import org.slf4j.Logger; @@ -68,6 +66,7 @@ public class OpenPgpMessageInputStream extends InputStream { protected final OpenPgpMetadata.Builder resultBuilder; // Pushdown Automaton to verify validity of OpenPGP packet sequence in an OpenPGP message protected final PDA automaton = new PDA(); + protected final DelayedTeeInputStreamInputStream delayedTee; // InputStream of OpenPGP packets of the current layer protected final BCPGInputStream packetInputStream; // InputStream of a nested data packet @@ -96,8 +95,9 @@ public class OpenPgpMessageInputStream extends InputStream { this.signatures.addDetachedSignatures(options.getDetachedSignatures()); } - BCPGInputStream bcpg = BCPGInputStream.wrap(inputStream); - this.packetInputStream = new TeeBCPGInputStream(bcpg, signatures); + delayedTee = new DelayedTeeInputStreamInputStream(inputStream, signatures); + BCPGInputStream bcpg = BCPGInputStream.wrap(delayedTee); + this.packetInputStream = bcpg; // *omnomnom* consumePackets(); @@ -121,22 +121,15 @@ public class OpenPgpMessageInputStream extends InputStream { */ private void consumePackets() throws IOException, PGPException { - int tag; - loop: while ((tag = nextTag()) != -1) { - OpenPgpPacket nextPacket; - try { - nextPacket = OpenPgpPacket.requireFromTag(tag); - } catch (NoSuchElementException e) { - log("Invalid tag: " + tag); - throw e; - } - log(nextPacket.toString()); + OpenPgpPacket nextPacket; + loop: while ((nextPacket = nextPacketTag()) != null) { signatures.nextPacket(nextPacket); switch (nextPacket) { // Literal Data - the literal data content is the new input stream case LIT: automaton.next(InputAlphabet.LiteralData); + delayedTee.squeeze(); processLiteralData(); break loop; @@ -145,12 +138,14 @@ public class OpenPgpMessageInputStream extends InputStream { automaton.next(InputAlphabet.CompressedData); signatures.commitNested(); processCompressedData(); + delayedTee.squeeze(); break loop; // One Pass Signature case OPS: automaton.next(InputAlphabet.OnePassSignatures); PGPOnePassSignature onePassSignature = readOnePassSignature(); + delayedTee.squeeze(); signatures.addOnePassSignature(onePassSignature); break; @@ -160,6 +155,7 @@ public class OpenPgpMessageInputStream extends InputStream { automaton.next(InputAlphabet.Signatures); PGPSignature signature = readSignature(); + delayedTee.squeeze(); processSignature(signature, isSigForOPS); break; @@ -170,6 +166,7 @@ public class OpenPgpMessageInputStream extends InputStream { case SED: case SEIPD: automaton.next(InputAlphabet.EncryptedData); + delayedTee.squeeze(); if (processEncryptedData()) { break loop; } @@ -179,6 +176,7 @@ public class OpenPgpMessageInputStream extends InputStream { // Marker Packets need to be skipped and ignored case MARKER: packetInputStream.readPacket(); // skip + delayedTee.squeeze(); break; // Key Packets are illegal in this context @@ -206,6 +204,23 @@ public class OpenPgpMessageInputStream extends InputStream { } } + private OpenPgpPacket nextPacketTag() throws IOException { + int tag = nextTag(); + if (tag == -1) { + log("EOF"); + return null; + } + OpenPgpPacket packet; + try { + packet = OpenPgpPacket.requireFromTag(tag); + } catch (NoSuchElementException e) { + log("Invalid tag: " + tag); + throw e; + } + log(packet.toString()); + return packet; + } + private void processSignature(PGPSignature signature, boolean isSigForOPS) { if (isSigForOPS) { signatures.popNested(); @@ -229,41 +244,6 @@ public class OpenPgpMessageInputStream extends InputStream { nestedInputStream = literalData.getDataStream(); } - private void debugEncryptedData() throws PGPException, IOException { - PGPEncryptedDataList encDataList = new PGPEncryptedDataList(packetInputStream); - - // TODO: Replace with !encDataList.isIntegrityProtected() - if (!encDataList.get(0).isIntegrityProtected()) { - throw new MessageNotIntegrityProtectedException(); - } - - SortedESKs esks = new SortedESKs(encDataList); - for (PGPPublicKeyEncryptedData pkesk : esks.pkesks) { - long keyId = pkesk.getKeyID(); - PGPSecretKeyRing decryptionKeys = getDecryptionKey(keyId); - if (decryptionKeys == null) { - continue; - } - SecretKeyRingProtector protector = options.getSecretKeyProtector(decryptionKeys); - PGPSecretKey decryptionKey = decryptionKeys.getSecretKey(keyId); - PGPPrivateKey privateKey = UnlockSecretKey.unlockSecretKey(decryptionKey, protector); - - PublicKeyDataDecryptorFactory decryptorFactory = ImplementationFactory.getInstance() - .getPublicKeyDataDecryptorFactory(privateKey); - try { - InputStream decrypted = pkesk.getDataStream(decryptorFactory); - InputStream decoder = PGPUtil.getDecoderStream(decrypted); - PGPObjectFactory objectFactory = ImplementationFactory.getInstance() - .getPGPObjectFactory(decoder); - objectFactory.nextObject(); - objectFactory.nextObject(); - objectFactory.nextObject(); - } catch (PGPException e) { - // hm :/ - } - } - } - private boolean processEncryptedData() throws IOException, PGPException { PGPEncryptedDataList encDataList = new PGPEncryptedDataList(packetInputStream); @@ -553,12 +533,13 @@ public class OpenPgpMessageInputStream extends InputStream { // for literal data. UUUUUGLY!!!! private static final class Signatures extends OutputStream { final ConsumerOptions options; - final List detachedSignatures; - final List prependedSignatures; + final List detachedSignatures; + final List prependedSignatures; final List onePassSignatures; final Stack> opsUpdateStack; List literalOPS = new ArrayList<>(); final List correspondingSignatures; + boolean isLiteral = true; private Signatures(ConsumerOptions options) { this.options = options; @@ -579,14 +560,14 @@ public class OpenPgpMessageInputStream extends InputStream { long keyId = SignatureUtils.determineIssuerKeyId(signature); PGPPublicKeyRing certificate = findCertificate(keyId); initialize(signature, certificate, keyId); - this.detachedSignatures.add(signature); + this.detachedSignatures.add(new SIG(signature)); } void addPrependedSignature(PGPSignature signature) { long keyId = SignatureUtils.determineIssuerKeyId(signature); PGPPublicKeyRing certificate = findCertificate(keyId); initialize(signature, certificate, keyId); - this.prependedSignatures.add(signature); + this.prependedSignatures.add(new SIG(signature)); } void addOnePassSignature(PGPOnePassSignature signature) { @@ -630,7 +611,7 @@ public class OpenPgpMessageInputStream extends InputStream { opsUpdateStack.pop(); } - private void initialize(PGPSignature signature, PGPPublicKeyRing certificate, long keyId) { + private static void initialize(PGPSignature signature, PGPPublicKeyRing certificate, long keyId) { if (certificate == null) { // SHIT return; @@ -672,7 +653,7 @@ public class OpenPgpMessageInputStream extends InputStream { ops.update(b); } - for (PGPSignature detached : detachedSignatures) { + for (SIG detached : detachedSignatures) { detached.update(b); } } @@ -682,13 +663,22 @@ public class OpenPgpMessageInputStream extends InputStream { ops.update(b, off, len); } - for (PGPSignature detached : detachedSignatures) { + for (SIG detached : detachedSignatures) { detached.update(b, off, len); } } public void updatePacket(byte b) { - for (List nestedOPSs : opsUpdateStack) { + for (SIG detached : detachedSignatures) { + detached.update(b); + } + + for (SIG prepended : prependedSignatures) { + prepended.update(b); + } + + for (int i = opsUpdateStack.size() - 1; i >= 0; i--) { + List nestedOPSs = opsUpdateStack.get(i); for (OPS ops : nestedOPSs) { ops.update(b); } @@ -696,6 +686,14 @@ public class OpenPgpMessageInputStream extends InputStream { } public void updatePacket(byte[] buf, int off, int len) { + for (SIG detached : detachedSignatures) { + detached.update(buf, off, len); + } + + for (SIG prepended : prependedSignatures) { + prepended.update(buf, off, len); + } + for (int i = opsUpdateStack.size() - 1; i >= 0; i--) { List nestedOPSs = opsUpdateStack.get(i); for (OPS ops : nestedOPSs) { @@ -705,24 +703,16 @@ public class OpenPgpMessageInputStream extends InputStream { } public void finish() { - for (PGPSignature detached : detachedSignatures) { - boolean verified = false; - try { - verified = detached.verify(); - } catch (PGPException e) { - log("Cannot verify detached signature.", e); - } - log("Detached Signature by " + Long.toHexString(detached.getKeyID()) + " is " + (verified ? "verified" : "unverified")); + for (SIG detached : detachedSignatures) { + boolean verified = detached.verify(); + log("Detached Signature by " + Long.toHexString(detached.signature.getKeyID()) + " is " + (verified ? "verified" : "unverified")); + System.out.println(detached); } - for (PGPSignature prepended : prependedSignatures) { - boolean verified = false; - try { - verified = prepended.verify(); - } catch (PGPException e) { - log("Cannot verify prepended signature.", e); - } - log("Prepended Signature by " + Long.toHexString(prepended.getKeyID()) + " is " + (verified ? "verified" : "unverified")); + for (SIG prepended : prependedSignatures) { + boolean verified = prepended.verify(); + log("Prepended Signature by " + Long.toHexString(prepended.signature.getKeyID()) + " is " + (verified ? "verified" : "unverified")); + System.out.println(prepended); } } @@ -738,9 +728,92 @@ public class OpenPgpMessageInputStream extends InputStream { public void nextPacket(OpenPgpPacket nextPacket) { if (nextPacket == OpenPgpPacket.LIT) { + isLiteral = true; if (literalOPS.isEmpty() && !opsUpdateStack.isEmpty()) { literalOPS = opsUpdateStack.pop(); } + } else { + isLiteral = false; + } + } + + static class SIG { + ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + PGPSignature signature; + boolean finished; + boolean valid; + + public SIG(PGPSignature signature) { + this.signature = signature; + } + + public void init(PGPPublicKeyRing certificate) { + initialize(signature, certificate, signature.getKeyID()); + } + + public boolean verify() { + finished = true; + try { + valid = this.signature.verify(); + } catch (PGPException e) { + log("Cannot verify SIG " + signature.getKeyID()); + } + return valid; + } + + public void update(byte b) { + if (finished) { + log("Updating finished sig!"); + return; + } + signature.update(b); + bytes.write(b); + } + + public void update(byte[] bytes, int off, int len) { + if (finished) { + log("Updating finished sig!"); + return; + } + signature.update(bytes, off, len); + this.bytes.write(bytes, off, len); + } + + @Override + public String toString() { + String OPS = "c40d03000a01fbfcc82a015e733001"; + String LIT_H = "cb28620000000000"; + String LIT = "656e637279707420e28898207369676e20e28898207369676e20e28898207369676e"; + String SIG1 = "c2c10400010a006f058262c806350910fbfcc82a015e7330471400000000001e002073616c74406e6f746174696f6e732e736571756f69612d7067702e6f7267b0409ed8ea96dac66447bdff5b7b60c9f80a0ab91d257029153dc3b6d8c27b98162104d1a66e1a23b182c9980f788cfbfcc82a015e7330000029640c00846b5096d92474fd446cc7edaf9f14572cab93a80e12384c1e829f95debc6e8373c2ce5402be53dc1a18cf92a0ed909e0fb38855713ef8ffb13502ffac7c830fa254cc1aa6c666a97b0cc3bc176538f6913d3b8e8981a65cc42df10e0f39e4d0a06dfe961437b59a71892f4fca1116aed15123ea0d86a7b2ce47dd9d3ef22d920631bc011e82babe03ad5d72b3ba7f95bf646f20ccf6f7a4d95de37397c76c7d53741458e51ab6074007f61181c7b88b7c98f5b7510c8dfa3be01f4841501679478b15c5249d928e2a10d15ec63efa1500b994d5bfb32ffb174a976116930eb97a111e6dfd4c5e43e04a5d76ba74806a62fda63a8c3f53f6eebaf852892340e81dd08bbf348454a2cf525aeb512cf33aeeee78465ee4c442e41cc45ac4e3bb0c3333677aa60332ee7f464d9020f8554b82d619872477cca18d8431888f4ae8abe5894e9720f759c410cd7991db12703dc147040dd0d3758223e0b75de6ceae49c1a0c2c45efedeb7114ae785cc886afdc45c82172e4476e1ab5b86dc4314dd76"; + String SIG1f = "c2c13b0400010a006f058262c806350910fbfcc82a015e7330471400000000001e002073616c74406e6f746174696f6e732e736571756f69612d7067702e6f7267b0409ed8ea96dac66447bdff5b7b60c9f80a0ab91d257029153dc3b6d8c27b98162104d1a66e1a23b182c9980f788cfbfcc82a015e7330000029640c00846b5096d92474fd446cc7edaf9f14572cab93a80e12384c1e829f95debc6e8373c2ce5402be53dc1a18cf92a0ed909e0fb38855713ef8ffb13502ffac7c830fa254cc1aa6c666a97b0cc3bc176538f6913d3b8e8981a65cc42df10e0f39e4d0a06dfe961437b59a71892f4fca1116aed15123ea0d86a7b2ce47dd9d3ef22d920631bc011e82babe03ad5d72b3ba7f95bf646f20ccf6f7a4d95de37397c76c7d53741458e51ab6074007f61181c7b88b7c98f5b7510c8dfa3be01f4841501679478b15c5249d928e2a10d15ec63efa1500b994d5bfb32ffb174a976116930eb97a111e6dfd4c5e43e04a5d76ba74806a62fda63a8c3f53f6eebaf852892340e81dd08bbf348454a2cf525aeb512cf33aeeee78465ee4c442e41cc45ac4e3bb0c3333677aa60332ee7f464d9020f8554b82d619872477cca18d8431888f4ae8abe5894e9720f759c410cd7991db12703dc147040dd0d3758223e0b75de6ceae49c1a0c2c45efedeb7114ae785cc886afdc45c82172e4476e1ab5b86dc4314dd76"; + String SIG2 = "c2c10400010a006f058262c806350910fbfcc82a015e7330471400000000001e002073616c74406e6f746174696f6e732e736571756f69612d7067702e6f7267a4d9c117dc7ba3a7e9270856f128d2ab271743eac3cb5750b22a89bd5fd60753162104d1a66e1a23b182c9980f788cfbfcc82a015e73300000b8400bff796c20fa8b25ff7a42686338e06417a2966e85a0fc2723c928bef6cd19d34cf5e7d55ada33080613012dadb79e0278e59d9e7ed7d2d6102912a5f768c2e75b60099225c3d8bfe0c123240188b80dbee89b9b3bd5b13ccc662abc37e2129b6968adac9aba43aa778c0fe4fe337591ee87a96a29a013debc83555293c877144fc676aa1b03782c501949521a320adf6ad96c4f2e036b52a18369c637fdc49033696a84d03a69580b953187fce5aca6fb26fc8815da9f3b513bfe8e304f33ecb4b521aeb7d09c4a284ea66123bd0d6a358b2526d762ca110e1f7f20b3038d774b64d5cfd34e2213765828359d7afc5bf24d5270e99d80c3c1568fa01624b6ea1e9ce4e6890ce9bacf6611a45d41e2671f68f5b096446bf08d27ce75608425b2e3ab92146229ad1fcd8224aca5b5f73960506e7df07bfbf3664348e8ecbfb2eb467b9cfe412cb377a6ee2eb5fd11be9cf9208fe9a74c296f52cfa02a1eb0519ad9a8349bf6ccd6495feb7e391451bf96e08a0798883dee5974e47cbf3b51f111b6d3"; + String SIG2f = "c2c13b0400010a006f058262c806350910fbfcc82a015e7330471400000000001e002073616c74406e6f746174696f6e732e736571756f69612d7067702e6f7267a4d9c117dc7ba3a7e9270856f128d2ab271743eac3cb5750b22a89bd5fd60753162104d1a66e1a23b182c9980f788cfbfcc82a015e73300000b8400bff796c20fa8b25ff7a42686338e06417a2966e85a0fc2723c928bef6cd19d34cf5e7d55ada33080613012dadb79e0278e59d9e7ed7d2d6102912a5f768c2e75b60099225c3d8bfe0c123240188b80dbee89b9b3bd5b13ccc662abc37e2129b6968adac9aba43aa778c0fe4fe337591ee87a96a29a013debc83555293c877144fc676aa1b03782c501949521a320adf6ad96c4f2e036b52a18369c637fdc49033696a84d03a69580b953187fce5aca6fb26fc8815da9f3b513bfe8e304f33ecb4b521aeb7d09c4a284ea66123bd0d6a358b2526d762ca110e1f7f20b3038d774b64d5cfd34e2213765828359d7afc5bf24d5270e99d80c3c1568fa01624b6ea1e9ce4e6890ce9bacf6611a45d41e2671f68f5b096446bf08d27ce75608425b2e3ab92146229ad1fcd8224aca5b5f73960506e7df07bfbf3664348e8ecbfb2eb467b9cfe412cb377a6ee2eb5fd11be9cf9208fe9a74c296f52cfa02a1eb0519ad9a8349bf6ccd6495feb7e391451bf96e08a0798883dee5974e47cbf3b51f111b6d3"; + String out = ""; + + String hex = Hex.toHexString(bytes.toByteArray()); + while (hex.contains(OPS)) { + hex = hex.replace(OPS, "[OPS]"); + } + while (hex.contains(LIT_H)) { + hex = hex.replace(LIT_H, "[LIT]"); + } + while (hex.contains(LIT)) { + hex = hex.replace(LIT, ""); + } + while (hex.contains(SIG1)) { + hex = hex.replace(SIG1, "[SIG1]"); + } + while (hex.contains(SIG1f)) { + hex = hex.replace(SIG1f, "[SIG1f]"); + } + while (hex.contains(SIG2)) { + hex = hex.replace(SIG2, "[SIG2]"); + } + while (hex.contains(SIG2f)) { + hex = hex.replace(SIG2f, "[SIG2f]"); + } + + return out + hex; } } @@ -796,27 +869,35 @@ public class OpenPgpMessageInputStream extends InputStream { String LIT_H = "cb28620000000000"; String LIT = "656e637279707420e28898207369676e20e28898207369676e20e28898207369676e"; String SIG1 = "c2c10400010a006f058262c806350910fbfcc82a015e7330471400000000001e002073616c74406e6f746174696f6e732e736571756f69612d7067702e6f7267b0409ed8ea96dac66447bdff5b7b60c9f80a0ab91d257029153dc3b6d8c27b98162104d1a66e1a23b182c9980f788cfbfcc82a015e7330000029640c00846b5096d92474fd446cc7edaf9f14572cab93a80e12384c1e829f95debc6e8373c2ce5402be53dc1a18cf92a0ed909e0fb38855713ef8ffb13502ffac7c830fa254cc1aa6c666a97b0cc3bc176538f6913d3b8e8981a65cc42df10e0f39e4d0a06dfe961437b59a71892f4fca1116aed15123ea0d86a7b2ce47dd9d3ef22d920631bc011e82babe03ad5d72b3ba7f95bf646f20ccf6f7a4d95de37397c76c7d53741458e51ab6074007f61181c7b88b7c98f5b7510c8dfa3be01f4841501679478b15c5249d928e2a10d15ec63efa1500b994d5bfb32ffb174a976116930eb97a111e6dfd4c5e43e04a5d76ba74806a62fda63a8c3f53f6eebaf852892340e81dd08bbf348454a2cf525aeb512cf33aeeee78465ee4c442e41cc45ac4e3bb0c3333677aa60332ee7f464d9020f8554b82d619872477cca18d8431888f4ae8abe5894e9720f759c410cd7991db12703dc147040dd0d3758223e0b75de6ceae49c1a0c2c45efedeb7114ae785cc886afdc45c82172e4476e1ab5b86dc4314dd76"; + String SIG1f = "c2c13b0400010a006f058262c806350910fbfcc82a015e7330471400000000001e002073616c74406e6f746174696f6e732e736571756f69612d7067702e6f7267b0409ed8ea96dac66447bdff5b7b60c9f80a0ab91d257029153dc3b6d8c27b98162104d1a66e1a23b182c9980f788cfbfcc82a015e7330000029640c00846b5096d92474fd446cc7edaf9f14572cab93a80e12384c1e829f95debc6e8373c2ce5402be53dc1a18cf92a0ed909e0fb38855713ef8ffb13502ffac7c830fa254cc1aa6c666a97b0cc3bc176538f6913d3b8e8981a65cc42df10e0f39e4d0a06dfe961437b59a71892f4fca1116aed15123ea0d86a7b2ce47dd9d3ef22d920631bc011e82babe03ad5d72b3ba7f95bf646f20ccf6f7a4d95de37397c76c7d53741458e51ab6074007f61181c7b88b7c98f5b7510c8dfa3be01f4841501679478b15c5249d928e2a10d15ec63efa1500b994d5bfb32ffb174a976116930eb97a111e6dfd4c5e43e04a5d76ba74806a62fda63a8c3f53f6eebaf852892340e81dd08bbf348454a2cf525aeb512cf33aeeee78465ee4c442e41cc45ac4e3bb0c3333677aa60332ee7f464d9020f8554b82d619872477cca18d8431888f4ae8abe5894e9720f759c410cd7991db12703dc147040dd0d3758223e0b75de6ceae49c1a0c2c45efedeb7114ae785cc886afdc45c82172e4476e1ab5b86dc4314dd76"; String SIG2 = "c2c10400010a006f058262c806350910fbfcc82a015e7330471400000000001e002073616c74406e6f746174696f6e732e736571756f69612d7067702e6f7267a4d9c117dc7ba3a7e9270856f128d2ab271743eac3cb5750b22a89bd5fd60753162104d1a66e1a23b182c9980f788cfbfcc82a015e73300000b8400bff796c20fa8b25ff7a42686338e06417a2966e85a0fc2723c928bef6cd19d34cf5e7d55ada33080613012dadb79e0278e59d9e7ed7d2d6102912a5f768c2e75b60099225c3d8bfe0c123240188b80dbee89b9b3bd5b13ccc662abc37e2129b6968adac9aba43aa778c0fe4fe337591ee87a96a29a013debc83555293c877144fc676aa1b03782c501949521a320adf6ad96c4f2e036b52a18369c637fdc49033696a84d03a69580b953187fce5aca6fb26fc8815da9f3b513bfe8e304f33ecb4b521aeb7d09c4a284ea66123bd0d6a358b2526d762ca110e1f7f20b3038d774b64d5cfd34e2213765828359d7afc5bf24d5270e99d80c3c1568fa01624b6ea1e9ce4e6890ce9bacf6611a45d41e2671f68f5b096446bf08d27ce75608425b2e3ab92146229ad1fcd8224aca5b5f73960506e7df07bfbf3664348e8ecbfb2eb467b9cfe412cb377a6ee2eb5fd11be9cf9208fe9a74c296f52cfa02a1eb0519ad9a8349bf6ccd6495feb7e391451bf96e08a0798883dee5974e47cbf3b51f111b6d3"; - String out = signature.getKeyID() + " last=" + signature.isContaining() + "\n"; + String SIG2f = "c2c13b0400010a006f058262c806350910fbfcc82a015e7330471400000000001e002073616c74406e6f746174696f6e732e736571756f69612d7067702e6f7267a4d9c117dc7ba3a7e9270856f128d2ab271743eac3cb5750b22a89bd5fd60753162104d1a66e1a23b182c9980f788cfbfcc82a015e73300000b8400bff796c20fa8b25ff7a42686338e06417a2966e85a0fc2723c928bef6cd19d34cf5e7d55ada33080613012dadb79e0278e59d9e7ed7d2d6102912a5f768c2e75b60099225c3d8bfe0c123240188b80dbee89b9b3bd5b13ccc662abc37e2129b6968adac9aba43aa778c0fe4fe337591ee87a96a29a013debc83555293c877144fc676aa1b03782c501949521a320adf6ad96c4f2e036b52a18369c637fdc49033696a84d03a69580b953187fce5aca6fb26fc8815da9f3b513bfe8e304f33ecb4b521aeb7d09c4a284ea66123bd0d6a358b2526d762ca110e1f7f20b3038d774b64d5cfd34e2213765828359d7afc5bf24d5270e99d80c3c1568fa01624b6ea1e9ce4e6890ce9bacf6611a45d41e2671f68f5b096446bf08d27ce75608425b2e3ab92146229ad1fcd8224aca5b5f73960506e7df07bfbf3664348e8ecbfb2eb467b9cfe412cb377a6ee2eb5fd11be9cf9208fe9a74c296f52cfa02a1eb0519ad9a8349bf6ccd6495feb7e391451bf96e08a0798883dee5974e47cbf3b51f111b6d3"; + String out = "last=" + signature.isContaining() + "\n"; - String hex = Hex.toHexString(bytes.toByteArray()); - while (hex.contains(OPS)) { - hex = hex.replace(OPS, "[OPS]"); - } - while (hex.contains(LIT_H)) { - hex = hex.replace(LIT_H, "[LIT]"); - } - while (hex.contains(LIT)) { - hex = hex.replace(LIT, ""); - } - while (hex.contains(SIG1)) { - hex = hex.replace(SIG1, "[SIG1]"); - } - while (hex.contains(SIG2)) { - hex = hex.replace(SIG2, "[SIG2]"); - } + String hex = Hex.toHexString(bytes.toByteArray()); + while (hex.contains(OPS)) { + hex = hex.replace(OPS, "[OPS]"); + } + while (hex.contains(LIT_H)) { + hex = hex.replace(LIT_H, "[LIT]"); + } + while (hex.contains(LIT)) { + hex = hex.replace(LIT, ""); + } + while (hex.contains(SIG1)) { + hex = hex.replace(SIG1, "[SIG1]"); + } + while (hex.contains(SIG1f)) { + hex = hex.replace(SIG1f, "[SIG1f]"); + } + while (hex.contains(SIG2)) { + hex = hex.replace(SIG2, "[SIG2]"); + } + while (hex.contains(SIG2f)) { + hex = hex.replace(SIG2f, "[SIG2f]"); + } - return out + hex; + return out + hex; } } } diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/TeeBCPGInputStream.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/TeeBCPGInputStream.java deleted file mode 100644 index cce21051..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/TeeBCPGInputStream.java +++ /dev/null @@ -1,36 +0,0 @@ -package org.pgpainless.decryption_verification; - -import org.bouncycastle.bcpg.BCPGInputStream; -import org.pgpainless.util.ArmorUtils; - -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; - -public class TeeBCPGInputStream extends BCPGInputStream { - - private final OutputStream out; - - public TeeBCPGInputStream(InputStream in, OutputStream outputStream) { - super(in); - this.out = outputStream; - } - - @Override - public int read() throws IOException { - int r = super.read(); - if (r != -1) { - out.write(r); - } - return r; - } - - @Override - public int read(byte[] buf, int off, int len) throws IOException { - int r = super.read(buf, off, len); - if (r > 0) { - out.write(buf, off, r); - } - return r; - } -} 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 f780b2eb..46c521ef 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 @@ -4,7 +4,6 @@ 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 static org.junit.jupiter.api.Assertions.assertTrue; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; @@ -143,11 +142,11 @@ public class OpenPgpMessageInputStreamTest { "-----BEGIN PGP MESSAGE-----\n" + "Version: PGPainless\n" + "\n" + - "hF4Dyqa/GWUy6WsSAQdAQ62BwmUt8Iby0+jvrLhMgST79KR/as+dyl0nf1uki2sw\n" + - "Thg1Ojtf0hOyJgcpQ4nP2Q0wYFR0F1sCydaIlTGreYZHlGtybP7/Ml6KNZILTRWP\n" + - "0kYBkGBgK7oQWRIVyoF2POvEP6EX1X8nvQk7O3NysVdRVbnia7gE3AzRYuha4kxs\n" + - "pI6xJkntLMS3K6him1Y9FHINIASFSB+C\n" + - "=5p00\n" + + "hF4Dyqa/GWUy6WsSAQdAuGt49sQwdAHH3jPx11V3wSh7Amur3TbnONiQYJmMo3Qw\n" + + "87yBnZCsaB7evxLBgi6PpF3tiytHM60xlrPeKKPpJhu60vNafRM2OOwqk7AdcZw4\n" + + "0kYBEhiioO2btSuafNrQEjYzAgC7K6l7aPCcQObNp4ofryXu1P5vN+vUZp357hyS\n" + + "6zZqP+0wJQ9yJZMvFTtFeSaSi0oMP2sb\n" + + "=LvRL\n" + "-----END PGP MESSAGE-----"; public static final String OPS_LIT_SIG = "" + @@ -170,8 +169,8 @@ public class OpenPgpMessageInputStreamTest { // genKey(); // genSIG_LIT(); // genSENC_LIT(); - // genPENC_COMP_LIT(); - genOPS_LIT_SIG(); + genPENC_COMP_LIT(); + // genOPS_LIT_SIG(); } public static void genLIT() throws IOException { @@ -433,91 +432,144 @@ public class OpenPgpMessageInputStreamTest { assertNull(metadata.getCompressionAlgorithm()); } + String BOB_KEY = "-----BEGIN PGP PRIVATE KEY BLOCK-----\n" + + "Comment: Bob's OpenPGP Transferable Secret Key\n" + + "\n" + + "lQVYBF2lnPIBDAC5cL9PQoQLTMuhjbYvb4Ncuuo0bfmgPRFywX53jPhoFf4Zg6mv\n" + + "/seOXpgecTdOcVttfzC8ycIKrt3aQTiwOG/ctaR4Bk/t6ayNFfdUNxHWk4WCKzdz\n" + + "/56fW2O0F23qIRd8UUJp5IIlN4RDdRCtdhVQIAuzvp2oVy/LaS2kxQoKvph/5pQ/\n" + + "5whqsyroEWDJoSV0yOb25B/iwk/pLUFoyhDG9bj0kIzDxrEqW+7Ba8nocQlecMF3\n" + + "X5KMN5kp2zraLv9dlBBpWW43XktjcCZgMy20SouraVma8Je/ECwUWYUiAZxLIlMv\n" + + "9CurEOtxUw6N3RdOtLmYZS9uEnn5y1UkF88o8Nku890uk6BrewFzJyLAx5wRZ4F0\n" + + "qV/yq36UWQ0JB/AUGhHVPdFf6pl6eaxBwT5GXvbBUibtf8YI2og5RsgTWtXfU7eb\n" + + "SGXrl5ZMpbA6mbfhd0R8aPxWfmDWiIOhBufhMCvUHh1sApMKVZnvIff9/0Dca3wb\n" + + "vLIwa3T4CyshfT0AEQEAAQAL/RZqbJW2IqQDCnJi4Ozm++gPqBPiX1RhTWSjwxfM\n" + + "cJKUZfzLj414rMKm6Jh1cwwGY9jekROhB9WmwaaKT8HtcIgrZNAlYzANGRCM4TLK\n" + + "3VskxfSwKKna8l+s+mZglqbAjUg3wmFuf9Tj2xcUZYmyRm1DEmcN2ZzpvRtHgX7z\n" + + "Wn1mAKUlSDJZSQks0zjuMNbupcpyJokdlkUg2+wBznBOTKzgMxVNC9b2g5/tMPUs\n" + + "hGGWmF1UH+7AHMTaS6dlmr2ZBIyogdnfUqdNg5sZwsxSNrbglKP4sqe7X61uEAIQ\n" + + "bD7rT3LonLbhkrj3I8wilUD8usIwt5IecoHhd9HziqZjRCc1BUBkboUEoyedbDV4\n" + + "i4qfsFZ6CEWoLuD5pW7dEp0M+WeuHXO164Rc+LnH6i1VQrpb1Okl4qO6ejIpIjBI\n" + + "1t3GshtUu/mwGBBxs60KBX5g77mFQ9lLCRj8lSYqOsHRKBhUp4qM869VA+fD0BRP\n" + + "fqPT0I9IH4Oa/A3jYJcg622GwQYA1LhnP208Waf6PkQSJ6kyr8ymY1yVh9VBE/g6\n" + + "fRDYA+pkqKnw9wfH2Qho3ysAA+OmVOX8Hldg+Pc0Zs0e5pCavb0En8iFLvTA0Q2E\n" + + "LR5rLue9uD7aFuKFU/VdcddY9Ww/vo4k5p/tVGp7F8RYCFn9rSjIWbfvvZi1q5Tx\n" + + "+akoZbga+4qQ4WYzB/obdX6SCmi6BndcQ1QdjCCQU6gpYx0MddVERbIp9+2SXDyL\n" + + "hpxjSyz+RGsZi/9UAshT4txP4+MZBgDfK3ZqtW+h2/eMRxkANqOJpxSjMyLO/FXN\n" + + "WxzTDYeWtHNYiAlOwlQZEPOydZFty9IVzzNFQCIUCGjQ/nNyhw7adSgUk3+BXEx/\n" + + "MyJPYY0BYuhLxLYcrfQ9nrhaVKxRJj25SVHj2ASsiwGJRZW4CC3uw40OYxfKEvNC\n" + + "mer/VxM3kg8qqGf9KUzJ1dVdAvjyx2Hz6jY2qWCyRQ6IMjWHyd43C4r3jxooYKUC\n" + + "YnstRQyb/gCSKahveSEjo07CiXMr88UGALwzEr3npFAsPW3osGaFLj49y1oRe11E\n" + + "he9gCHFm+fuzbXrWmdPjYU5/ZdqdojzDqfu4ThfnipknpVUM1o6MQqkjM896FHm8\n" + + "zbKVFSMhEP6DPHSCexMFrrSgN03PdwHTO6iBaIBBFqmGY01tmJ03SxvSpiBPON9P\n" + + "NVvy/6UZFedTq8A07OUAxO62YUSNtT5pmK2vzs3SAZJmbFbMh+NN204TRI72GlqT\n" + + "t5hcfkuv8hrmwPS/ZR6q312mKQ6w/1pqO9qitCFCb2IgQmFiYmFnZSA8Ym9iQG9w\n" + + "ZW5wZ3AuZXhhbXBsZT6JAc4EEwEKADgCGwMFCwkIBwIGFQoJCAsCBBYCAwECHgEC\n" + + "F4AWIQTRpm4aI7GCyZgPeIz7/MgqAV5zMAUCXaWe+gAKCRD7/MgqAV5zMG9sC/9U\n" + + "2T3RrqEbw533FPNfEflhEVRIZ8gDXKM8hU6cqqEzCmzZT6xYTe6sv4y+PJBGXJFX\n" + + "yhj0g6FDkSyboM5litOcTupURObVqMgA/Y4UKERznm4fzzH9qek85c4ljtLyNufe\n" + + "doL2pp3vkGtn7eD0QFRaLLmnxPKQ/TlZKdLE1G3u8Uot8QHicaR6GnAdc5UXQJE3\n" + + "BiV7jZuDyWmZ1cUNwJkKL6oRtp+ZNDOQCrLNLecKHcgCqrpjSQG5oouba1I1Q6Vl\n" + + "sP44dhA1nkmLHtxlTOzpeHj4jnk1FaXmyasurrrI5CgU/L2Oi39DGKTH/A/cywDN\n" + + "4ZplIQ9zR8enkbXquUZvFDe+Xz+6xRXtb5MwQyWODB3nHw85HocLwRoIN9WdQEI+\n" + + "L8a/56AuOwhs8llkSuiITjR7r9SgKJC2WlAHl7E8lhJ3VDW3ELC56KH308d6mwOG\n" + + "ZRAqIAKzM1T5FGjMBhq7ZV0eqdEntBh3EcOIfj2M8rg1MzJv+0mHZOIjByawikad\n" + + "BVgEXaWc8gEMANYwv1xsYyunXYK0X1vY/rP1NNPvhLyLIE7NpK90YNBj+xS1ldGD\n" + + "bUdZqZeef2xJe8gMQg05DoD1DF3GipZ0Ies65beh+d5hegb7N4pzh0LzrBrVNHar\n" + + "29b5ExdI7i4iYD5TO6Vr/qTUOiAN/byqELEzAb+L+b2DVz/RoCm4PIp1DU9ewcc2\n" + + "WB38Ofqut3nLYA5tqJ9XvAiEQme+qAVcM3ZFcaMt4I4dXhDZZNg+D9LiTWcxdUPB\n" + + "leu8iwDRjAgyAhPzpFp+nWoqWA81uIiULWD1Fj+IVoY3ZvgivoYOiEFBJ9lbb4te\n" + + "g9m5UT/AaVDTWuHzbspVlbiVe+qyB77C2daWzNyx6UYBPLOo4r0t0c91kbNE5lgj\n" + + "Z7xz6los0N1U8vq91EFSeQJoSQ62XWavYmlCLmdNT6BNfgh4icLsT7Vr1QMX9jzn\n" + + "JtTPxdXytSdHvpSpULsqJ016l0dtmONcK3z9mj5N5z0k1tg1AH970TGYOe2aUcSx\n" + + "IRDMXDOPyzEfjwARAQABAAv9F2CwsjS+Sjh1M1vegJbZjei4gF1HHpEM0K0PSXsp\n" + + "SfVvpR4AoSJ4He6CXSMWg0ot8XKtDuZoV9jnJaES5UL9pMAD7JwIOqZm/DYVJM5h\n" + + "OASCh1c356/wSbFbzRHPtUdZO9Q30WFNJM5pHbCJPjtNoRmRGkf71RxtvHBzy7np\n" + + "Ga+W6U/NVKHw0i0CYwMI0YlKDakYW3Pm+QL+gHZFvngGweTod0f9l2VLLAmeQR/c\n" + + "+EZs7lNumhuZ8mXcwhUc9JQIhOkpO+wreDysEFkAcsKbkQP3UDUsA1gFx9pbMzT0\n" + + "tr1oZq2a4QBtxShHzP/ph7KLpN+6qtjks3xB/yjTgaGmtrwM8tSe0wD1RwXS+/1o\n" + + "BHpXTnQ7TfeOGUAu4KCoOQLv6ELpKWbRBLWuiPwMdbGpvVFALO8+kvKAg9/r+/ny\n" + + "zM2GQHY+J3Jh5JxPiJnHfXNZjIKLbFbIPdSKNyJBuazXW8xIa//mEHMI5OcvsZBK\n" + + "clAIp7LXzjEjKXIwHwDcTn9pBgDpdOKTHOtJ3JUKx0rWVsDH6wq6iKV/FTVSY5jl\n" + + "zN+puOEsskF1Lfxn9JsJihAVO3yNsp6RvkKtyNlFazaCVKtDAmkjoh60XNxcNRqr\n" + + "gCnwdpbgdHP6v/hvZY54ZaJjz6L2e8unNEkYLxDt8cmAyGPgH2XgL7giHIp9jrsQ\n" + + "aS381gnYwNX6wE1aEikgtY91nqJjwPlibF9avSyYQoMtEqM/1UjTjB2KdD/MitK5\n" + + "fP0VpvuXpNYZedmyq4UOMwdkiNMGAOrfmOeT0olgLrTMT5H97Cn3Yxbk13uXHNu/\n" + + "ZUZZNe8s+QtuLfUlKAJtLEUutN33TlWQY522FV0m17S+b80xJib3yZVJteVurrh5\n" + + "HSWHAM+zghQAvCesg5CLXa2dNMkTCmZKgCBvfDLZuZbjFwnwCI6u/NhOY9egKuUf\n" + + "SA/je/RXaT8m5VxLYMxwqQXKApzD87fv0tLPlVIEvjEsaf992tFEFSNPcG1l/jpd\n" + + "5AVXw6kKuf85UkJtYR1x2MkQDrqY1QX/XMw00kt8y9kMZUre19aCArcmor+hDhRJ\n" + + "E3Gt4QJrD9z/bICESw4b4z2DbgD/Xz9IXsA/r9cKiM1h5QMtXvuhyfVeM01enhxM\n" + + "GbOH3gjqqGNKysx0UODGEwr6AV9hAd8RWXMchJLaExK9J5SRawSg671ObAU24SdY\n" + + "vMQ9Z4kAQ2+1ReUZzf3ogSMRZtMT+d18gT6L90/y+APZIaoArLPhebIAGq39HLmJ\n" + + "26x3z0WAgrpA1kNsjXEXkoiZGPLKIGoe3hqJAbYEGAEKACAWIQTRpm4aI7GCyZgP\n" + + "eIz7/MgqAV5zMAUCXaWc8gIbDAAKCRD7/MgqAV5zMOn/C/9ugt+HZIwX308zI+QX\n" + + "c5vDLReuzmJ3ieE0DMO/uNSC+K1XEioSIZP91HeZJ2kbT9nn9fuReuoff0T0Dief\n" + + "rbwcIQQHFFkrqSp1K3VWmUGp2JrUsXFVdjy/fkBIjTd7c5boWljv/6wAsSfiv2V0\n" + + "JSM8EFU6TYXxswGjFVfc6X97tJNeIrXL+mpSmPPqy2bztcCCHkWS5lNLWQw+R7Vg\n" + + "71Fe6yBSNVrqC2/imYG2J9zlowjx1XU63Wdgqp2Wxt0l8OmsB/W80S1fRF5G4SDH\n" + + "s9HXglXXqPsBRZJYfP+VStm9L5P/sKjCcX6WtZR7yS6G8zj/X767MLK/djANvpPd\n" + + "NVniEke6hM3CNBXYPAMhQBMWhCulcoz+0lxi8L34rMN+Dsbma96psdUrn7uLaB91\n" + + "6we0CTfF8qqm7BsVAgalon/UUiuMY80U3ueoj3okiSTiHIjD/YtpXSPioC8nMng7\n" + + "xqAY9Bwizt4FWgXuLm1a4+So4V9j1TRCXd12Uc2l2RNmgDE=\n" + + "=miES\n" + + "-----END PGP PRIVATE KEY BLOCK-----"; + + @ParameterizedTest(name = "Process PENC(OPS OPS LIT SIG SIG) using {0}") + @MethodSource("provideMessageProcessors") + public void testProcessPENC_OPS_OPS_LIT_SIG_SIG(Processor processor) throws IOException, PGPException { + String MSG = "-----BEGIN PGP MESSAGE-----\n" + + "\n" + + "wcDMA3wvqk35PDeyAQv/RhY9sgxMXj1UxumNMOeN+1+c5bB5e3jSrvA93L8yLFqB\n" + + "uF4MsFnHNgu3bS+/a3Z63MRdgS3wOxaRrvEE3y0Q316rP0OQxj9c2mMPZdHlIxjL\n" + + "KJMzQ6Ofs4kdtapo7plFqKBEEvnp7rF1hFAPxi0/Z+ekuhhOnWg6dZpAZH+s5Li0\n" + + "rKUltzFJ0bxPe6LCuwyYnzKnNBJJsQdKwcvX2Ip8+6lTX/DjQR1s5nhIe76GaNcU\n" + + "OvXITOynDsGgNfAmrqTVfrVgDvOVgvj46UPAwS02uYNNk8pWlcy4iGYIlQBUHD6P\n" + + "k1ieG7ETWsJvStceFqLQVgSDErAga/YXXAJnNUF3PnOxgOlVewdxDCoEeu+3OdQE\n" + + "j7hqmTTo3iA5GaTKCOi07NwXoXRhEMN3X6XDI5+ovqzAYaPkITxtqZzoNVKMT5hi\n" + + "tRKl0qwHbMsfHRCQesDmDPU4MlI7TH2iX2jMPxaepyAI++NMW7H6w8bYEFaE0O9v\n" + + "tiTL2gcYv4O/pGd3isWb0sOkAdz7HkKDdFCUdVMwP25z6dwhEy+oR/q1Le1CjCE/\n" + + "kY1bmJCTBmJwf86YGZElxFuvCTUBBX6ChI7+o18fljQE7eIS0GjXkQ1j2zEXxgGy\n" + + "Lhq7yCr6XEIVUj0x8J4LU2RthtgyToOH7EjLRUbqBG2PZD5K7L7b+ueLSkCfM5Gr\n" + + "isGbTTj6e+TLy6rXGxlNmNDoojpfp/5rRCxrmqPOjBZrNcio8rG19PfBkaw1IXu9\n" + + "fV9klsIxQyiOmUIl7sc74tTBwdIq8F6FJ7sJIScSCrzMjy+J+VLaBl1LyKs9cWDr\n" + + "vUqHvc9diwFWjbtZ8wQn9TQug5X4m6sT+pl+7UALAGWdyI9ySlSvVmVnGROKehkV\n" + + "5VfRds1ICH9Y4XAD7ylzF4dJ0gadtgwD97HLmfApP9IFD/sC4Oy2fu/ERky3Qqrw\n" + + "nvxDpFZBAzNiTR5VXlEPH2DeQUL0tyJJtq5InjqJm/F2K6O11Xk/HSm9VP3Bnhbc\n" + + "djaA7GTTYTq2MjPIDYq+ujPkD/WDp5a/2MIWS10ucgZIcLEwJeU/OY+98W/ogrd5\n" + + "tg03XkKLcGuK6sGv1iYsOGw1vI6RKAkI1j7YBXb7Twb3Ueq/lcRvutgMx/O5k0L5\n" + + "+d3kl6XJVQVKneft7C6DEu6boiGQCTtloJFxaJ9POqq6DzTQ5hSGvBNiUuek3HV7\n" + + "lHH544/ONgCufprT3cUSU0CW9EVbeHq3st3wKwxT5ei8nd8R+TuwaPI3TBSqeV03\n" + + "9fz5x9U2a22Uh53/qux2vAl8DyZHw7VWTP/Bu3eWHiDBEQIQY9BbRMYc7ueNwPii\n" + + "EROFOrHikkDr8UPwNC9FmpLd4vmQQfioY1bAuFvDckTrRFRp2ft+8m0oWLuF+3IH\n" + + "lJ2ph3w62VbIOmG0dxtI626n32NcPwk6shCP/gtW1ixuLr1OpiEe5slt2eNiPoTG\n" + + "CX5UnxzwUkyJ9KgLr3uFkMUwITCF9d2HbnHRaYqVDbQBpZW0wmgtpkTp2tNTExvp\n" + + "T2kx8LNHxAYNoSX+OOWvWzimkCO9MUfjpa0i5kVNxHronNcb1hKAU6X/2r2Mt3C4\n" + + "sv2m08spJBQJWnaa/8paYm+c8JS8oACD9SK/8Y4E1kNM3yEgk8dM2BLHKN3xkyT6\n" + + "iPXHKKgEHivTdpDa8gY81uoqorRHt5gNPDqL/p2ttFquBbQUtRvDCMkvqif5DADS\n" + + "wvLnnlOohCnQbFsNtWg5G6UUQ0TYbt6bixHpNcYIuFEJubJOJTuh/paxPgI3xx1q\n" + + "AdrStz97gowgNanOc+Quyt+zmb5cFQdAPLj76xv/W9zd4N601C1NE6+UhZ6mx/Ut\n" + + "wboetRk4HNcTRmBci5gjNoqB5oQnyAyqhHL1yiD3YmwwELnRwE8563HrHEpU6ziq\n" + + "D1pPMF6YBcmSuHp8FubPeef8iGHYEJQscRTIy/sb6YQjgShjE4VXfGJ2vOz3KRfU\n" + + "s7O7MH2b1YkDPsTDuLoDjBzDRoA+2vi034km9Qdcs3w8+vrydw4=\n" + + "=mdYs\n" + + "-----END PGP MESSAGE-----\n"; + PGPSecretKeyRing secretKeys = PGPainless.readKeyRing().secretKeyRing(BOB_KEY); + PGPPublicKeyRing certificate = PGPainless.extractCertificate(secretKeys); + + Tuple result = processor.process(MSG, ConsumerOptions.get() + .addVerificationCert(certificate) + .addDecryptionKey(secretKeys)); + String plain = result.getA(); + assertEquals("encrypt ∘ sign ∘ sign", plain); + MessageMetadata metadata = result.getB(); + assertEquals(SymmetricKeyAlgorithm.AES_256, metadata.getEncryptionAlgorithm()); + assertNull(metadata.getCompressionAlgorithm()); + } + @ParameterizedTest(name = "Process PENC(OPS OPS OPS LIT SIG SIG SIG) using {0}") @MethodSource("provideMessageProcessors") public void testProcessOPS_OPS_OPS_LIT_SIG_SIG_SIG(Processor processor) throws IOException, PGPException { - String KEY = "-----BEGIN PGP PRIVATE KEY BLOCK-----\n" + - "Comment: Bob's OpenPGP Transferable Secret Key\n" + - "\n" + - "lQVYBF2lnPIBDAC5cL9PQoQLTMuhjbYvb4Ncuuo0bfmgPRFywX53jPhoFf4Zg6mv\n" + - "/seOXpgecTdOcVttfzC8ycIKrt3aQTiwOG/ctaR4Bk/t6ayNFfdUNxHWk4WCKzdz\n" + - "/56fW2O0F23qIRd8UUJp5IIlN4RDdRCtdhVQIAuzvp2oVy/LaS2kxQoKvph/5pQ/\n" + - "5whqsyroEWDJoSV0yOb25B/iwk/pLUFoyhDG9bj0kIzDxrEqW+7Ba8nocQlecMF3\n" + - "X5KMN5kp2zraLv9dlBBpWW43XktjcCZgMy20SouraVma8Je/ECwUWYUiAZxLIlMv\n" + - "9CurEOtxUw6N3RdOtLmYZS9uEnn5y1UkF88o8Nku890uk6BrewFzJyLAx5wRZ4F0\n" + - "qV/yq36UWQ0JB/AUGhHVPdFf6pl6eaxBwT5GXvbBUibtf8YI2og5RsgTWtXfU7eb\n" + - "SGXrl5ZMpbA6mbfhd0R8aPxWfmDWiIOhBufhMCvUHh1sApMKVZnvIff9/0Dca3wb\n" + - "vLIwa3T4CyshfT0AEQEAAQAL/RZqbJW2IqQDCnJi4Ozm++gPqBPiX1RhTWSjwxfM\n" + - "cJKUZfzLj414rMKm6Jh1cwwGY9jekROhB9WmwaaKT8HtcIgrZNAlYzANGRCM4TLK\n" + - "3VskxfSwKKna8l+s+mZglqbAjUg3wmFuf9Tj2xcUZYmyRm1DEmcN2ZzpvRtHgX7z\n" + - "Wn1mAKUlSDJZSQks0zjuMNbupcpyJokdlkUg2+wBznBOTKzgMxVNC9b2g5/tMPUs\n" + - "hGGWmF1UH+7AHMTaS6dlmr2ZBIyogdnfUqdNg5sZwsxSNrbglKP4sqe7X61uEAIQ\n" + - "bD7rT3LonLbhkrj3I8wilUD8usIwt5IecoHhd9HziqZjRCc1BUBkboUEoyedbDV4\n" + - "i4qfsFZ6CEWoLuD5pW7dEp0M+WeuHXO164Rc+LnH6i1VQrpb1Okl4qO6ejIpIjBI\n" + - "1t3GshtUu/mwGBBxs60KBX5g77mFQ9lLCRj8lSYqOsHRKBhUp4qM869VA+fD0BRP\n" + - "fqPT0I9IH4Oa/A3jYJcg622GwQYA1LhnP208Waf6PkQSJ6kyr8ymY1yVh9VBE/g6\n" + - "fRDYA+pkqKnw9wfH2Qho3ysAA+OmVOX8Hldg+Pc0Zs0e5pCavb0En8iFLvTA0Q2E\n" + - "LR5rLue9uD7aFuKFU/VdcddY9Ww/vo4k5p/tVGp7F8RYCFn9rSjIWbfvvZi1q5Tx\n" + - "+akoZbga+4qQ4WYzB/obdX6SCmi6BndcQ1QdjCCQU6gpYx0MddVERbIp9+2SXDyL\n" + - "hpxjSyz+RGsZi/9UAshT4txP4+MZBgDfK3ZqtW+h2/eMRxkANqOJpxSjMyLO/FXN\n" + - "WxzTDYeWtHNYiAlOwlQZEPOydZFty9IVzzNFQCIUCGjQ/nNyhw7adSgUk3+BXEx/\n" + - "MyJPYY0BYuhLxLYcrfQ9nrhaVKxRJj25SVHj2ASsiwGJRZW4CC3uw40OYxfKEvNC\n" + - "mer/VxM3kg8qqGf9KUzJ1dVdAvjyx2Hz6jY2qWCyRQ6IMjWHyd43C4r3jxooYKUC\n" + - "YnstRQyb/gCSKahveSEjo07CiXMr88UGALwzEr3npFAsPW3osGaFLj49y1oRe11E\n" + - "he9gCHFm+fuzbXrWmdPjYU5/ZdqdojzDqfu4ThfnipknpVUM1o6MQqkjM896FHm8\n" + - "zbKVFSMhEP6DPHSCexMFrrSgN03PdwHTO6iBaIBBFqmGY01tmJ03SxvSpiBPON9P\n" + - "NVvy/6UZFedTq8A07OUAxO62YUSNtT5pmK2vzs3SAZJmbFbMh+NN204TRI72GlqT\n" + - "t5hcfkuv8hrmwPS/ZR6q312mKQ6w/1pqO9qitCFCb2IgQmFiYmFnZSA8Ym9iQG9w\n" + - "ZW5wZ3AuZXhhbXBsZT6JAc4EEwEKADgCGwMFCwkIBwIGFQoJCAsCBBYCAwECHgEC\n" + - "F4AWIQTRpm4aI7GCyZgPeIz7/MgqAV5zMAUCXaWe+gAKCRD7/MgqAV5zMG9sC/9U\n" + - "2T3RrqEbw533FPNfEflhEVRIZ8gDXKM8hU6cqqEzCmzZT6xYTe6sv4y+PJBGXJFX\n" + - "yhj0g6FDkSyboM5litOcTupURObVqMgA/Y4UKERznm4fzzH9qek85c4ljtLyNufe\n" + - "doL2pp3vkGtn7eD0QFRaLLmnxPKQ/TlZKdLE1G3u8Uot8QHicaR6GnAdc5UXQJE3\n" + - "BiV7jZuDyWmZ1cUNwJkKL6oRtp+ZNDOQCrLNLecKHcgCqrpjSQG5oouba1I1Q6Vl\n" + - "sP44dhA1nkmLHtxlTOzpeHj4jnk1FaXmyasurrrI5CgU/L2Oi39DGKTH/A/cywDN\n" + - "4ZplIQ9zR8enkbXquUZvFDe+Xz+6xRXtb5MwQyWODB3nHw85HocLwRoIN9WdQEI+\n" + - "L8a/56AuOwhs8llkSuiITjR7r9SgKJC2WlAHl7E8lhJ3VDW3ELC56KH308d6mwOG\n" + - "ZRAqIAKzM1T5FGjMBhq7ZV0eqdEntBh3EcOIfj2M8rg1MzJv+0mHZOIjByawikad\n" + - "BVgEXaWc8gEMANYwv1xsYyunXYK0X1vY/rP1NNPvhLyLIE7NpK90YNBj+xS1ldGD\n" + - "bUdZqZeef2xJe8gMQg05DoD1DF3GipZ0Ies65beh+d5hegb7N4pzh0LzrBrVNHar\n" + - "29b5ExdI7i4iYD5TO6Vr/qTUOiAN/byqELEzAb+L+b2DVz/RoCm4PIp1DU9ewcc2\n" + - "WB38Ofqut3nLYA5tqJ9XvAiEQme+qAVcM3ZFcaMt4I4dXhDZZNg+D9LiTWcxdUPB\n" + - "leu8iwDRjAgyAhPzpFp+nWoqWA81uIiULWD1Fj+IVoY3ZvgivoYOiEFBJ9lbb4te\n" + - "g9m5UT/AaVDTWuHzbspVlbiVe+qyB77C2daWzNyx6UYBPLOo4r0t0c91kbNE5lgj\n" + - "Z7xz6los0N1U8vq91EFSeQJoSQ62XWavYmlCLmdNT6BNfgh4icLsT7Vr1QMX9jzn\n" + - "JtTPxdXytSdHvpSpULsqJ016l0dtmONcK3z9mj5N5z0k1tg1AH970TGYOe2aUcSx\n" + - "IRDMXDOPyzEfjwARAQABAAv9F2CwsjS+Sjh1M1vegJbZjei4gF1HHpEM0K0PSXsp\n" + - "SfVvpR4AoSJ4He6CXSMWg0ot8XKtDuZoV9jnJaES5UL9pMAD7JwIOqZm/DYVJM5h\n" + - "OASCh1c356/wSbFbzRHPtUdZO9Q30WFNJM5pHbCJPjtNoRmRGkf71RxtvHBzy7np\n" + - "Ga+W6U/NVKHw0i0CYwMI0YlKDakYW3Pm+QL+gHZFvngGweTod0f9l2VLLAmeQR/c\n" + - "+EZs7lNumhuZ8mXcwhUc9JQIhOkpO+wreDysEFkAcsKbkQP3UDUsA1gFx9pbMzT0\n" + - "tr1oZq2a4QBtxShHzP/ph7KLpN+6qtjks3xB/yjTgaGmtrwM8tSe0wD1RwXS+/1o\n" + - "BHpXTnQ7TfeOGUAu4KCoOQLv6ELpKWbRBLWuiPwMdbGpvVFALO8+kvKAg9/r+/ny\n" + - "zM2GQHY+J3Jh5JxPiJnHfXNZjIKLbFbIPdSKNyJBuazXW8xIa//mEHMI5OcvsZBK\n" + - "clAIp7LXzjEjKXIwHwDcTn9pBgDpdOKTHOtJ3JUKx0rWVsDH6wq6iKV/FTVSY5jl\n" + - "zN+puOEsskF1Lfxn9JsJihAVO3yNsp6RvkKtyNlFazaCVKtDAmkjoh60XNxcNRqr\n" + - "gCnwdpbgdHP6v/hvZY54ZaJjz6L2e8unNEkYLxDt8cmAyGPgH2XgL7giHIp9jrsQ\n" + - "aS381gnYwNX6wE1aEikgtY91nqJjwPlibF9avSyYQoMtEqM/1UjTjB2KdD/MitK5\n" + - "fP0VpvuXpNYZedmyq4UOMwdkiNMGAOrfmOeT0olgLrTMT5H97Cn3Yxbk13uXHNu/\n" + - "ZUZZNe8s+QtuLfUlKAJtLEUutN33TlWQY522FV0m17S+b80xJib3yZVJteVurrh5\n" + - "HSWHAM+zghQAvCesg5CLXa2dNMkTCmZKgCBvfDLZuZbjFwnwCI6u/NhOY9egKuUf\n" + - "SA/je/RXaT8m5VxLYMxwqQXKApzD87fv0tLPlVIEvjEsaf992tFEFSNPcG1l/jpd\n" + - "5AVXw6kKuf85UkJtYR1x2MkQDrqY1QX/XMw00kt8y9kMZUre19aCArcmor+hDhRJ\n" + - "E3Gt4QJrD9z/bICESw4b4z2DbgD/Xz9IXsA/r9cKiM1h5QMtXvuhyfVeM01enhxM\n" + - "GbOH3gjqqGNKysx0UODGEwr6AV9hAd8RWXMchJLaExK9J5SRawSg671ObAU24SdY\n" + - "vMQ9Z4kAQ2+1ReUZzf3ogSMRZtMT+d18gT6L90/y+APZIaoArLPhebIAGq39HLmJ\n" + - "26x3z0WAgrpA1kNsjXEXkoiZGPLKIGoe3hqJAbYEGAEKACAWIQTRpm4aI7GCyZgP\n" + - "eIz7/MgqAV5zMAUCXaWc8gIbDAAKCRD7/MgqAV5zMOn/C/9ugt+HZIwX308zI+QX\n" + - "c5vDLReuzmJ3ieE0DMO/uNSC+K1XEioSIZP91HeZJ2kbT9nn9fuReuoff0T0Dief\n" + - "rbwcIQQHFFkrqSp1K3VWmUGp2JrUsXFVdjy/fkBIjTd7c5boWljv/6wAsSfiv2V0\n" + - "JSM8EFU6TYXxswGjFVfc6X97tJNeIrXL+mpSmPPqy2bztcCCHkWS5lNLWQw+R7Vg\n" + - "71Fe6yBSNVrqC2/imYG2J9zlowjx1XU63Wdgqp2Wxt0l8OmsB/W80S1fRF5G4SDH\n" + - "s9HXglXXqPsBRZJYfP+VStm9L5P/sKjCcX6WtZR7yS6G8zj/X767MLK/djANvpPd\n" + - "NVniEke6hM3CNBXYPAMhQBMWhCulcoz+0lxi8L34rMN+Dsbma96psdUrn7uLaB91\n" + - "6we0CTfF8qqm7BsVAgalon/UUiuMY80U3ueoj3okiSTiHIjD/YtpXSPioC8nMng7\n" + - "xqAY9Bwizt4FWgXuLm1a4+So4V9j1TRCXd12Uc2l2RNmgDE=\n" + - "=miES\n" + - "-----END PGP PRIVATE KEY BLOCK-----"; String MSG = "-----BEGIN PGP MESSAGE-----\n" + "\n" + "wcDMA3wvqk35PDeyAQwA0yaEgydkAMEfl7rDTYVGanLKiFiWIs34mkF+LB8qR5eY\n" + @@ -565,7 +617,7 @@ public class OpenPgpMessageInputStreamTest { "x12WVuyITVU3fCfHp6/0A6wPtJezCvoodqPlw/3fd5eSVYzb5C3v564uhz4=\n" + "=JP9T\n" + "-----END PGP MESSAGE-----"; - PGPSecretKeyRing secretKeys = PGPainless.readKeyRing().secretKeyRing(KEY); + PGPSecretKeyRing secretKeys = PGPainless.readKeyRing().secretKeyRing(BOB_KEY); PGPPublicKeyRing certificate = PGPainless.extractCertificate(secretKeys); Tuple result = processor.process(MSG, ConsumerOptions.get() diff --git a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/TeeBCPGInputStreamTest.java b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/TeeBCPGInputStreamTest.java deleted file mode 100644 index d31c6009..00000000 --- a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/TeeBCPGInputStreamTest.java +++ /dev/null @@ -1,61 +0,0 @@ -package org.pgpainless.decryption_verification; - -import org.bouncycastle.bcpg.ArmoredInputStream; -import org.bouncycastle.bcpg.ArmoredOutputStream; -import org.bouncycastle.bcpg.BCPGInputStream; -import org.bouncycastle.bcpg.Packet; -import org.bouncycastle.openpgp.PGPCompressedData; -import org.bouncycastle.openpgp.PGPException; -import org.junit.jupiter.api.Test; -import org.pgpainless.algorithm.OpenPgpPacket; -import org.pgpainless.util.ArmoredInputStreamFactory; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.nio.charset.StandardCharsets; - -public class TeeBCPGInputStreamTest { - - private static final Logger LOGGER = LoggerFactory.getLogger(TeeBCPGInputStreamTest.class); - private static final String INBAND_SIGNED = "-----BEGIN PGP MESSAGE-----\n" + - "Version: PGPainless\n" + - "\n" + - "owGbwMvMyCUWdXSHvVTUtXbG0yJJDCDgkZqTk6+jEJ5flJOiyNVRysIoxsXAxsqU\n" + - "GDiVjUGRUwCmQUyRRWnOn9Z/PIseF3Yz6cCEL05nZDj1OClo75WVTjNmJPemW6qV\n" + - "6ki//1K1++2s0qTP+0N11O4z/BVLDDdxnmQryS+5VXjBX7/0Hxnm/eqeX6Zum35r\n" + - "M8e7ufwA\n" + - "=RDiy\n" + - "-----END PGP MESSAGE-----"; - - @Test - public void test() throws IOException, PGPException { - ByteArrayOutputStream out = new ByteArrayOutputStream(); - ArmoredOutputStream armorOut = new ArmoredOutputStream(out); - - ByteArrayInputStream bytesIn = new ByteArrayInputStream(INBAND_SIGNED.getBytes(StandardCharsets.UTF_8)); - ArmoredInputStream armorIn = ArmoredInputStreamFactory.get(bytesIn); - BCPGInputStream bcpgIn = new BCPGInputStream(armorIn); - TeeBCPGInputStream teeIn = new TeeBCPGInputStream(bcpgIn, armorOut); - - ByteArrayOutputStream nestedOut = new ByteArrayOutputStream(); - ArmoredOutputStream nestedArmorOut = new ArmoredOutputStream(nestedOut); - - PGPCompressedData compressedData = new PGPCompressedData(teeIn); - InputStream nestedStream = compressedData.getDataStream(); - BCPGInputStream nestedBcpgIn = new BCPGInputStream(nestedStream); - TeeBCPGInputStream nestedTeeIn = new TeeBCPGInputStream(nestedBcpgIn, nestedArmorOut); - - int tag; - while ((tag = nestedTeeIn.nextPacketTag()) != -1) { - LOGGER.debug(OpenPgpPacket.requireFromTag(tag).toString()); - Packet packet = nestedTeeIn.readPacket(); - } - - nestedArmorOut.close(); - LOGGER.debug(nestedOut.toString()); - } -} From ef310f201f9d4d2f31150aa382ee091b55bb158f Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Sat, 8 Oct 2022 13:57:53 +0200 Subject: [PATCH 25/80] Create TeeBCPGInputStream to move teeing logic out of OpenPgpMessageInputStream --- .../DelayedTeeInputStreamInputStream.java | 38 ----- .../OpenPgpMessageInputStream.java | 107 +++++--------- .../TeeBCPGInputStream.java | 131 ++++++++++++++++++ .../automaton/InputAlphabet.java | 4 +- .../automaton/PDA.java | 18 +-- .../OpenPgpMessageInputStreamTest.java | 3 +- .../automaton/PDATest.java | 10 +- 7 files changed, 182 insertions(+), 129 deletions(-) delete mode 100644 pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DelayedTeeInputStreamInputStream.java create mode 100644 pgpainless-core/src/main/java/org/pgpainless/decryption_verification/TeeBCPGInputStream.java diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DelayedTeeInputStreamInputStream.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DelayedTeeInputStreamInputStream.java deleted file mode 100644 index 5cbabf89..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DelayedTeeInputStreamInputStream.java +++ /dev/null @@ -1,38 +0,0 @@ -package org.pgpainless.decryption_verification; - -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; - -public class DelayedTeeInputStreamInputStream extends InputStream { - - private int last = -1; - private final InputStream inputStream; - private final OutputStream outputStream; - - public DelayedTeeInputStreamInputStream(InputStream inputStream, OutputStream outputStream) { - this.inputStream = inputStream; - this.outputStream = outputStream; - } - - @Override - public int read() throws IOException { - if (last != -1) { - outputStream.write(last); - } - last = inputStream.read(); - return last; - } - - /** - * Squeeze the last byte out and update the output stream. - * - * @throws IOException in case of an IO error - */ - public void squeeze() throws IOException { - if (last != -1) { - outputStream.write(last); - } - last = -1; - } -} 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 8d370965..75359de1 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 @@ -11,7 +11,6 @@ import java.io.OutputStream; import java.util.ArrayList; import java.util.Collection; import java.util.List; -import java.util.NoSuchElementException; import java.util.Stack; import org.bouncycastle.bcpg.BCPGInputStream; @@ -57,6 +56,8 @@ import org.pgpainless.util.Tuple; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import javax.annotation.Nonnull; + public class OpenPgpMessageInputStream extends InputStream { private static final Logger LOGGER = LoggerFactory.getLogger(OpenPgpMessageInputStream.class); @@ -66,16 +67,15 @@ public class OpenPgpMessageInputStream extends InputStream { protected final OpenPgpMetadata.Builder resultBuilder; // Pushdown Automaton to verify validity of OpenPGP packet sequence in an OpenPGP message protected final PDA automaton = new PDA(); - protected final DelayedTeeInputStreamInputStream delayedTee; - // InputStream of OpenPGP packets of the current layer - protected final BCPGInputStream packetInputStream; + // InputStream of OpenPGP packets + protected TeeBCPGInputStream packetInputStream; // InputStream of a nested data packet protected InputStream nestedInputStream; private boolean closed = false; private final Signatures signatures; - private MessageMetadata.Layer metadata; + private final MessageMetadata.Layer metadata; public OpenPgpMessageInputStream(InputStream inputStream, ConsumerOptions options) throws IOException, PGPException { @@ -95,9 +95,7 @@ public class OpenPgpMessageInputStream extends InputStream { this.signatures.addDetachedSignatures(options.getDetachedSignatures()); } - delayedTee = new DelayedTeeInputStreamInputStream(inputStream, signatures); - BCPGInputStream bcpg = BCPGInputStream.wrap(delayedTee); - this.packetInputStream = bcpg; + packetInputStream = new TeeBCPGInputStream(BCPGInputStream.wrap(inputStream), signatures); // *omnomnom* consumePackets(); @@ -122,41 +120,36 @@ public class OpenPgpMessageInputStream extends InputStream { private void consumePackets() throws IOException, PGPException { OpenPgpPacket nextPacket; - loop: while ((nextPacket = nextPacketTag()) != null) { + loop: while ((nextPacket = packetInputStream.nextPacketTag()) != null) { signatures.nextPacket(nextPacket); switch (nextPacket) { // Literal Data - the literal data content is the new input stream case LIT: automaton.next(InputAlphabet.LiteralData); - delayedTee.squeeze(); processLiteralData(); break loop; // Compressed Data - the content contains another OpenPGP message case COMP: automaton.next(InputAlphabet.CompressedData); - signatures.commitNested(); processCompressedData(); - delayedTee.squeeze(); break loop; // One Pass Signature case OPS: - automaton.next(InputAlphabet.OnePassSignatures); + automaton.next(InputAlphabet.OnePassSignature); PGPOnePassSignature onePassSignature = readOnePassSignature(); - delayedTee.squeeze(); signatures.addOnePassSignature(onePassSignature); break; // Signature - either prepended to the message, or corresponding to a One Pass Signature case SIG: + // true if Signature corresponds to OnePassSignature boolean isSigForOPS = automaton.peekStack() == StackAlphabet.ops; - automaton.next(InputAlphabet.Signatures); + automaton.next(InputAlphabet.Signature); - PGPSignature signature = readSignature(); - delayedTee.squeeze(); - processSignature(signature, isSigForOPS); + processSignature(isSigForOPS); break; @@ -166,7 +159,6 @@ public class OpenPgpMessageInputStream extends InputStream { case SED: case SEIPD: automaton.next(InputAlphabet.EncryptedData); - delayedTee.squeeze(); if (processEncryptedData()) { break loop; } @@ -175,8 +167,7 @@ public class OpenPgpMessageInputStream extends InputStream { // Marker Packets need to be skipped and ignored case MARKER: - packetInputStream.readPacket(); // skip - delayedTee.squeeze(); + packetInputStream.readMarker(); break; // Key Packets are illegal in this context @@ -204,26 +195,10 @@ public class OpenPgpMessageInputStream extends InputStream { } } - private OpenPgpPacket nextPacketTag() throws IOException { - int tag = nextTag(); - if (tag == -1) { - log("EOF"); - return null; - } - OpenPgpPacket packet; - try { - packet = OpenPgpPacket.requireFromTag(tag); - } catch (NoSuchElementException e) { - log("Invalid tag: " + tag); - throw e; - } - log(packet.toString()); - return packet; - } - - private void processSignature(PGPSignature signature, boolean isSigForOPS) { + private void processSignature(boolean isSigForOPS) throws PGPException, IOException { + PGPSignature signature = readSignature(); if (isSigForOPS) { - signatures.popNested(); + signatures.leaveNesting(); // TODO: Only leave nesting if all OPSs are dealt with signatures.addCorrespondingOnePassSignature(signature); } else { signatures.addPrependedSignature(signature); @@ -231,21 +206,22 @@ public class OpenPgpMessageInputStream extends InputStream { } private void processCompressedData() throws IOException, PGPException { - PGPCompressedData compressedData = new PGPCompressedData(packetInputStream); + signatures.enterNesting(); + PGPCompressedData compressedData = packetInputStream.readCompressedData(); MessageMetadata.CompressedData compressionLayer = new MessageMetadata.CompressedData( CompressionAlgorithm.fromId(compressedData.getAlgorithm())); nestedInputStream = new OpenPgpMessageInputStream(compressedData.getDataStream(), options, compressionLayer); } private void processLiteralData() throws IOException { - PGPLiteralData literalData = new PGPLiteralData(packetInputStream); + PGPLiteralData literalData = packetInputStream.readLiteralData(); this.metadata.setChild(new MessageMetadata.LiteralData(literalData.getFileName(), literalData.getModificationTime(), StreamEncoding.requireFromCode(literalData.getFormat()))); nestedInputStream = literalData.getDataStream(); } private boolean processEncryptedData() throws IOException, PGPException { - PGPEncryptedDataList encDataList = new PGPEncryptedDataList(packetInputStream); + PGPEncryptedDataList encDataList = packetInputStream.readEncryptedDataList(); // TODO: Replace with !encDataList.isIntegrityProtected() if (!encDataList.get(0).isIntegrityProtected()) { @@ -345,19 +321,6 @@ public class OpenPgpMessageInputStream extends InputStream { return false; } - private int nextTag() throws IOException { - try { - return packetInputStream.nextPacketTag(); - } catch (IOException e) { - if ("Stream closed".equals(e.getMessage())) { - // ZipInflater Streams sometimes close under our feet -.- - // Therefore we catch resulting IOEs and return -1 instead. - return -1; - } - throw e; - } - } - private List> findPotentialDecryptionKeys(PGPPublicKeyEncryptedData pkesk) { int algorithm = pkesk.getAlgorithm(); List> decryptionKeyCandidates = new ArrayList<>(); @@ -387,12 +350,12 @@ public class OpenPgpMessageInputStream extends InputStream { private PGPOnePassSignature readOnePassSignature() throws PGPException, IOException { - return new PGPOnePassSignature(packetInputStream); + return packetInputStream.readOnePassSignature(); } private PGPSignature readSignature() throws PGPException, IOException { - return new PGPSignature(packetInputStream); + return packetInputStream.readSignature(); } @Override @@ -428,7 +391,7 @@ public class OpenPgpMessageInputStream extends InputStream { } @Override - public int read(byte[] b, int off, int len) + public int read(@Nonnull byte[] b, int off, int len) throws IOException { if (nestedInputStream == null) { @@ -482,8 +445,7 @@ public class OpenPgpMessageInputStream extends InputStream { private void collectMetadata() { if (nestedInputStream instanceof OpenPgpMessageInputStream) { OpenPgpMessageInputStream child = (OpenPgpMessageInputStream) nestedInputStream; - MessageMetadata.Layer childLayer = child.metadata; - this.metadata.setChild((MessageMetadata.Nested) childLayer); + this.metadata.setChild((MessageMetadata.Nested) child.metadata); } } @@ -496,9 +458,9 @@ public class OpenPgpMessageInputStream extends InputStream { private static class SortedESKs { - private List skesks = new ArrayList<>(); - private List pkesks = new ArrayList<>(); - private List anonPkesks = new ArrayList<>(); + private final List skesks = new ArrayList<>(); + private final List pkesks = new ArrayList<>(); + private final List anonPkesks = new ArrayList<>(); SortedESKs(PGPEncryptedDataList esks) { for (PGPEncryptedData esk : esks) { @@ -526,11 +488,10 @@ public class OpenPgpMessageInputStream extends InputStream { } } - // TODO: In 'OPS LIT("Foo") SIG', OPS is only updated with "Foo" - // In 'OPS[1] OPS LIT("Foo") SIG SIG', OPS[1] (nested) is updated with OPS LIT("Foo") SIG. - // Therefore, we need to handle the innermost signature layer differently when updating with Literal data. - // For this we might want to provide two update entries into the Signatures class, one for OpenPGP packets and one - // for literal data. UUUUUGLY!!!! + // In 'OPS LIT("Foo") SIG', OPS is only updated with "Foo" + // In 'OPS[1] OPS LIT("Foo") SIG SIG', OPS[1] (nested) is updated with OPS LIT("Foo") SIG. + // Therefore, we need to handle the innermost signature layer differently when updating with Literal data. + // Furthermore, For 'OPS COMP(LIT("Foo")) SIG', the signature is updated with "Foo". CHAOS!!! private static final class Signatures extends OutputStream { final ConsumerOptions options; final List detachedSignatures; @@ -578,7 +539,7 @@ public class OpenPgpMessageInputStream extends InputStream { literalOPS.add(ops); if (signature.isContaining()) { - commitNested(); + enterNesting(); } } @@ -599,12 +560,12 @@ public class OpenPgpMessageInputStream extends InputStream { } } - void commitNested() { + void enterNesting() { opsUpdateStack.push(literalOPS); literalOPS = new ArrayList<>(); } - void popNested() { + void leaveNesting() { if (opsUpdateStack.isEmpty()) { return; } @@ -722,7 +683,7 @@ public class OpenPgpMessageInputStream extends InputStream { } @Override - public void write(byte[] b, int off, int len) { + public void write(@Nonnull byte[] b, int off, int len) { updatePacket(b, off, len); } diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/TeeBCPGInputStream.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/TeeBCPGInputStream.java new file mode 100644 index 00000000..f80793f0 --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/TeeBCPGInputStream.java @@ -0,0 +1,131 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.decryption_verification; + +import org.bouncycastle.bcpg.BCPGInputStream; +import org.bouncycastle.bcpg.MarkerPacket; +import org.bouncycastle.bcpg.Packet; +import org.bouncycastle.openpgp.PGPCompressedData; +import org.bouncycastle.openpgp.PGPEncryptedDataList; +import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.PGPLiteralData; +import org.bouncycastle.openpgp.PGPOnePassSignature; +import org.bouncycastle.openpgp.PGPSignature; +import org.pgpainless.algorithm.OpenPgpPacket; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.NoSuchElementException; + +/** + * Since we need to update signatures with data from the underlying stream, this class is used to tee out the data. + * Unfortunately we cannot simply override {@link BCPGInputStream#read()} to tee the data out though, since + * {@link BCPGInputStream#readPacket()} inconsistently calls a mix of {@link BCPGInputStream#read()} and + * {@link InputStream#read()} of the underlying stream. This would cause the second length byte to get swallowed up. + * + * Therefore, this class delegates the teeing to an {@link DelayedTeeInputStreamInputStream} which wraps the underlying + * stream. Since calling {@link BCPGInputStream#nextPacketTag()} reads up to and including the next packets tag, + * we need to delay teeing out that byte to signature verifiers. + * Hence, the reading methods of the {@link TeeBCPGInputStream} handle pushing this byte to the output stream using + * {@link DelayedTeeInputStreamInputStream#squeeze()}. + */ +public class TeeBCPGInputStream { + + protected final DelayedTeeInputStreamInputStream delayedTee; + // InputStream of OpenPGP packets of the current layer + protected final BCPGInputStream packetInputStream; + + public TeeBCPGInputStream(BCPGInputStream inputStream, OutputStream outputStream) { + this.delayedTee = new DelayedTeeInputStreamInputStream(inputStream, outputStream); + this.packetInputStream = BCPGInputStream.wrap(delayedTee); + } + + public OpenPgpPacket nextPacketTag() throws IOException { + int tag = packetInputStream.nextPacketTag(); + if (tag == -1) { + return null; + } + + OpenPgpPacket packet; + try { + packet = OpenPgpPacket.requireFromTag(tag); + } catch (NoSuchElementException e) { + throw e; + } + return packet; + } + + public Packet readPacket() throws IOException { + return packetInputStream.readPacket(); + } + + public PGPCompressedData readCompressedData() throws IOException { + delayedTee.squeeze(); + PGPCompressedData compressedData = new PGPCompressedData(packetInputStream); + return compressedData; + } + + public PGPLiteralData readLiteralData() throws IOException { + delayedTee.squeeze(); + return new PGPLiteralData(packetInputStream); + } + + public PGPEncryptedDataList readEncryptedDataList() throws IOException { + delayedTee.squeeze(); + return new PGPEncryptedDataList(packetInputStream); + } + + public PGPOnePassSignature readOnePassSignature() throws PGPException, IOException { + PGPOnePassSignature onePassSignature = new PGPOnePassSignature(packetInputStream); + delayedTee.squeeze(); + return onePassSignature; + } + + public PGPSignature readSignature() throws PGPException, IOException { + PGPSignature signature = new PGPSignature(packetInputStream); + delayedTee.squeeze(); + return signature; + } + + public MarkerPacket readMarker() throws IOException { + MarkerPacket markerPacket = (MarkerPacket) readPacket(); + delayedTee.squeeze(); + return markerPacket; + } + + public static class DelayedTeeInputStreamInputStream extends InputStream { + + private int last = -1; + private final InputStream inputStream; + private final OutputStream outputStream; + + public DelayedTeeInputStreamInputStream(InputStream inputStream, OutputStream outputStream) { + this.inputStream = inputStream; + this.outputStream = outputStream; + } + + @Override + public int read() throws IOException { + if (last != -1) { + outputStream.write(last); + } + last = inputStream.read(); + return last; + } + + /** + * Squeeze the last byte out and update the output stream. + * + * @throws IOException in case of an IO error + */ + public void squeeze() throws IOException { + if (last != -1) { + outputStream.write(last); + } + last = -1; + } + } +} diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/automaton/InputAlphabet.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/automaton/InputAlphabet.java index 8e795f5b..ad2a8c55 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/automaton/InputAlphabet.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/automaton/InputAlphabet.java @@ -18,11 +18,11 @@ public enum InputAlphabet { /** * A {@link PGPSignatureList} object. */ - Signatures, + Signature, /** * A {@link PGPOnePassSignatureList} object. */ - OnePassSignatures, + OnePassSignature, /** * A {@link PGPCompressedData} packet. * The contents of this packet MUST form a valid OpenPGP message, so a nested PDA is opened to verify 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 1d4b1189..156dc2ed 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 @@ -35,11 +35,11 @@ public class PDA { case LiteralData: return LiteralMessage; - case Signatures: + case Signature: automaton.pushStack(msg); return OpenPgpMessage; - case OnePassSignatures: + case OnePassSignature: automaton.pushStack(ops); automaton.pushStack(msg); return OpenPgpMessage; @@ -63,7 +63,7 @@ public class PDA { StackAlphabet stackItem = automaton.popStack(); switch (input) { - case Signatures: + case Signature: if (stackItem == ops) { return CorrespondingSignature; } else { @@ -78,7 +78,7 @@ public class PDA { } case LiteralData: - case OnePassSignatures: + case OnePassSignature: case CompressedData: case EncryptedData: default: @@ -92,7 +92,7 @@ public class PDA { State transition(InputAlphabet input, PDA automaton) throws MalformedOpenPgpMessageException { StackAlphabet stackItem = automaton.popStack(); switch (input) { - case Signatures: + case Signature: if (stackItem == ops) { return CorrespondingSignature; } else { @@ -107,7 +107,7 @@ public class PDA { } case LiteralData: - case OnePassSignatures: + case OnePassSignature: case CompressedData: case EncryptedData: default: @@ -121,7 +121,7 @@ public class PDA { State transition(InputAlphabet input, PDA automaton) throws MalformedOpenPgpMessageException { StackAlphabet stackItem = automaton.popStack(); switch (input) { - case Signatures: + case Signature: if (stackItem == ops) { return CorrespondingSignature; } else { @@ -136,7 +136,7 @@ public class PDA { } case LiteralData: - case OnePassSignatures: + case OnePassSignature: case CompressedData: case EncryptedData: default: @@ -156,7 +156,7 @@ public class PDA { // premature end of stream throw new MalformedOpenPgpMessageException(this, input, stackItem); } - } else if (input == InputAlphabet.Signatures) { + } else if (input == InputAlphabet.Signature) { if (stackItem == ops) { return CorrespondingSignature; } 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 46c521ef..1955eebe 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 @@ -49,7 +49,6 @@ import org.pgpainless.util.Tuple; public class OpenPgpMessageInputStreamTest { - public static final String KEY = "-----BEGIN PGP PRIVATE KEY BLOCK-----\n" + "Version: PGPainless\n" + "Comment: DA05 848F 37D4 68E6 F982 C889 7A70 1FC6 904D 3F4C\n" + @@ -241,7 +240,7 @@ public class OpenPgpMessageInputStreamTest { System.out); } - public static void genSIG_LIT() throws PGPException, IOException { + public static void genSIG_COMP_LIT() throws PGPException, IOException { PGPSecretKeyRing secretKeys = PGPainless.readKeyRing().secretKeyRing(KEY); ByteArrayOutputStream msgOut = new ByteArrayOutputStream(); EncryptionStream signer = PGPainless.encryptAndOrSign() diff --git a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/automaton/PDATest.java b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/automaton/PDATest.java index 6e8a38d6..6dbb2cd6 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/automaton/PDATest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/automaton/PDATest.java @@ -30,9 +30,9 @@ public class PDATest { @Test public void testSimpleOpsSignedMesssageIsValid() throws MalformedOpenPgpMessageException { PDA automaton = new PDA(); - automaton.next(InputAlphabet.OnePassSignatures); + automaton.next(InputAlphabet.OnePassSignature); automaton.next(InputAlphabet.LiteralData); - automaton.next(InputAlphabet.Signatures); + automaton.next(InputAlphabet.Signature); automaton.next(InputAlphabet.EndOfSequence); assertTrue(automaton.isValid()); @@ -47,7 +47,7 @@ public class PDATest { @Test public void testSimplePrependSignedMessageIsValid() throws MalformedOpenPgpMessageException { PDA automaton = new PDA(); - automaton.next(InputAlphabet.Signatures); + automaton.next(InputAlphabet.Signature); automaton.next(InputAlphabet.LiteralData); automaton.next(InputAlphabet.EndOfSequence); @@ -63,10 +63,10 @@ public class PDATest { @Test public void testOPSSignedCompressedMessageIsValid() throws MalformedOpenPgpMessageException { PDA automaton = new PDA(); - automaton.next(InputAlphabet.OnePassSignatures); + automaton.next(InputAlphabet.OnePassSignature); automaton.next(InputAlphabet.CompressedData); // Here would be a nested PDA for the LiteralData packet - automaton.next(InputAlphabet.Signatures); + automaton.next(InputAlphabet.Signature); automaton.next(InputAlphabet.EndOfSequence); assertTrue(automaton.isValid()); From f4ce669d44504bcdab28e43720ba8470fd7d8f28 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Sun, 16 Oct 2022 15:47:47 +0200 Subject: [PATCH 26/80] It was the buffering. --- .../DecryptionBuilderInterface.java | 2 +- .../DecryptionStream.java | 60 +------ .../DecryptionStreamFactory.java | 33 +++- .../DecryptionStreamImpl.java | 65 +++++++ .../MessageMetadata.java | 22 +++ .../OpenPgpMessageInputStream.java | 166 ++++++++++++++---- ...vestigateMultiSEIPMessageHandlingTest.java | 17 +- ...ntDecryptionUsingNonEncryptionKeyTest.java | 4 +- 8 files changed, 256 insertions(+), 113 deletions(-) create mode 100644 pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionStreamImpl.java diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionBuilderInterface.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionBuilderInterface.java index 07db42f0..b35911de 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionBuilderInterface.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionBuilderInterface.java @@ -13,7 +13,7 @@ import org.bouncycastle.openpgp.PGPException; public interface DecryptionBuilderInterface { /** - * Create a {@link DecryptionStream} on an {@link InputStream} which contains the encrypted and/or signed data. + * Create a {@link DecryptionStreamImpl} on an {@link InputStream} which contains the encrypted and/or signed data. * * @param inputStream encrypted and/or signed data. * @return api handle diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionStream.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionStream.java index e3f1e720..c531f487 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionStream.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionStream.java @@ -1,65 +1,9 @@ -// SPDX-FileCopyrightText: 2018 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - package org.pgpainless.decryption_verification; -import java.io.IOException; -import java.io.InputStream; import javax.annotation.Nonnull; -import org.bouncycastle.util.io.Streams; - -/** - * Decryption Stream that handles updating and verification of detached signatures, - * as well as verification of integrity-protected input streams once the stream gets closed. - */ -public class DecryptionStream extends CloseForResultInputStream { - - private final InputStream inputStream; - private final IntegrityProtectedInputStream integrityProtectedInputStream; - private final InputStream armorStream; - - /** - * Create an input stream that handles decryption and - if necessary - integrity protection verification. - * - * @param wrapped underlying input stream - * @param resultBuilder builder for decryption metadata like algorithms, recipients etc. - * @param integrityProtectedInputStream in case of data encrypted using SEIP packet close this stream to check integrity - * @param armorStream armor stream to verify CRC checksums - */ - DecryptionStream(@Nonnull InputStream wrapped, - @Nonnull OpenPgpMetadata.Builder resultBuilder, - IntegrityProtectedInputStream integrityProtectedInputStream, - InputStream armorStream) { +public abstract class DecryptionStream extends CloseForResultInputStream { + public DecryptionStream(@Nonnull OpenPgpMetadata.Builder resultBuilder) { super(resultBuilder); - this.inputStream = wrapped; - this.integrityProtectedInputStream = integrityProtectedInputStream; - this.armorStream = armorStream; } - - @Override - public void close() throws IOException { - if (armorStream != null) { - Streams.drain(armorStream); - } - inputStream.close(); - if (integrityProtectedInputStream != null) { - integrityProtectedInputStream.close(); - } - super.close(); - } - - @Override - public int read() throws IOException { - int r = inputStream.read(); - return r; - } - - @Override - public int read(@Nonnull byte[] bytes, int offset, int length) throws IOException { - int read = inputStream.read(bytes, offset, length); - return read; - } - } 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 07be7c10..680f33e4 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 @@ -40,6 +40,7 @@ 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.graalvm.compiler.lir.amd64.AMD64BinaryConsumer; import org.pgpainless.PGPainless; import org.pgpainless.algorithm.CompressionAlgorithm; import org.pgpainless.algorithm.EncryptionPurpose; @@ -88,8 +89,28 @@ public final class DecryptionStreamFactory { public static DecryptionStream create(@Nonnull InputStream inputStream, - @Nonnull ConsumerOptions options) + @Nonnull ConsumerOptions options) throws PGPException, IOException { + OpenPgpInputStream openPgpInputStream = new OpenPgpInputStream(inputStream); + openPgpInputStream.reset(); + if (openPgpInputStream.isBinaryOpenPgp()) { + return new OpenPgpMessageInputStream(openPgpInputStream, options); + } else if (openPgpInputStream.isAsciiArmored()) { + ArmoredInputStream armorIn = ArmoredInputStreamFactory.get(openPgpInputStream); + if (armorIn.isClearText()) { + return createOld(openPgpInputStream, options); + } else { + return new OpenPgpMessageInputStream(armorIn, options); + } + } else if (openPgpInputStream.isNonOpenPgp()) { + return createOld(openPgpInputStream, options); + } else { + throw new IOException("What?"); + } + } + + public static DecryptionStream createOld(@Nonnull InputStream inputStream, + @Nonnull ConsumerOptions options) throws IOException, PGPException { DecryptionStreamFactory factory = new DecryptionStreamFactory(options); OpenPgpInputStream openPgpIn = new OpenPgpInputStream(inputStream); return factory.parseOpenPGPDataAndCreateDecryptionStream(openPgpIn); @@ -136,7 +157,7 @@ public final class DecryptionStreamFactory { if (openPgpIn.isNonOpenPgp() || options.isForceNonOpenPgpData()) { outerDecodingStream = openPgpIn; pgpInStream = wrapInVerifySignatureStream(outerDecodingStream, null); - return new DecryptionStream(pgpInStream, resultBuilder, integrityProtectedEncryptedInputStream, null); + return new DecryptionStreamImpl(pgpInStream, resultBuilder, integrityProtectedEncryptedInputStream, null); } // Data appears to be OpenPGP message, @@ -147,7 +168,7 @@ public final class DecryptionStreamFactory { objectFactory = ImplementationFactory.getInstance().getPGPObjectFactory(outerDecodingStream); // Parse OpenPGP message pgpInStream = processPGPPackets(objectFactory, 1); - return new DecryptionStream(pgpInStream, + return new DecryptionStreamImpl(pgpInStream, resultBuilder, integrityProtectedEncryptedInputStream, null); } @@ -161,7 +182,7 @@ public final class DecryptionStreamFactory { objectFactory = ImplementationFactory.getInstance().getPGPObjectFactory(outerDecodingStream); // Parse OpenPGP message pgpInStream = processPGPPackets(objectFactory, 1); - return new DecryptionStream(pgpInStream, + return new DecryptionStreamImpl(pgpInStream, resultBuilder, integrityProtectedEncryptedInputStream, outerDecodingStream); } @@ -170,7 +191,7 @@ public final class DecryptionStreamFactory { throw new PGPException("Not sure how to handle the input stream."); } - private DecryptionStream parseCleartextSignedMessage(ArmoredInputStream armorIn) + private DecryptionStreamImpl parseCleartextSignedMessage(ArmoredInputStream armorIn) throws IOException, PGPException { resultBuilder.setCompressionAlgorithm(CompressionAlgorithm.UNCOMPRESSED) .setFileEncoding(StreamEncoding.TEXT); @@ -185,7 +206,7 @@ public final class DecryptionStreamFactory { initializeDetachedSignatures(options.getDetachedSignatures()); InputStream verifyIn = wrapInVerifySignatureStream(multiPassStrategy.getMessageInputStream(), null); - return new DecryptionStream(verifyIn, resultBuilder, integrityProtectedEncryptedInputStream, + return new DecryptionStreamImpl(verifyIn, resultBuilder, integrityProtectedEncryptedInputStream, null); } diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionStreamImpl.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionStreamImpl.java new file mode 100644 index 00000000..27ace697 --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionStreamImpl.java @@ -0,0 +1,65 @@ +// SPDX-FileCopyrightText: 2018 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.decryption_verification; + +import java.io.IOException; +import java.io.InputStream; +import javax.annotation.Nonnull; + +import org.bouncycastle.util.io.Streams; + +/** + * Decryption Stream that handles updating and verification of detached signatures, + * as well as verification of integrity-protected input streams once the stream gets closed. + */ +public class DecryptionStreamImpl extends DecryptionStream { + + private final InputStream inputStream; + private final IntegrityProtectedInputStream integrityProtectedInputStream; + private final InputStream armorStream; + + /** + * Create an input stream that handles decryption and - if necessary - integrity protection verification. + * + * @param wrapped underlying input stream + * @param resultBuilder builder for decryption metadata like algorithms, recipients etc. + * @param integrityProtectedInputStream in case of data encrypted using SEIP packet close this stream to check integrity + * @param armorStream armor stream to verify CRC checksums + */ + DecryptionStreamImpl(@Nonnull InputStream wrapped, + @Nonnull OpenPgpMetadata.Builder resultBuilder, + IntegrityProtectedInputStream integrityProtectedInputStream, + InputStream armorStream) { + super(resultBuilder); + this.inputStream = wrapped; + this.integrityProtectedInputStream = integrityProtectedInputStream; + this.armorStream = armorStream; + } + + @Override + public void close() throws IOException { + if (armorStream != null) { + Streams.drain(armorStream); + } + inputStream.close(); + if (integrityProtectedInputStream != null) { + integrityProtectedInputStream.close(); + } + super.close(); + } + + @Override + public int read() throws IOException { + int r = inputStream.read(); + return r; + } + + @Override + public int read(@Nonnull byte[] bytes, int offset, int length) throws IOException { + int read = inputStream.read(bytes, offset, length); + return read; + } + +} 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 index cbf251a8..87f6d7f6 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/MessageMetadata.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/MessageMetadata.java @@ -69,6 +69,28 @@ public class MessageMetadata { }; } + public @Nullable SessionKey getSessionKey() { + Iterator sessionKeys = getSessionKeys(); + if (sessionKeys.hasNext()) { + return sessionKeys.next(); + } + return null; + } + + public @Nonnull Iterator getSessionKeys() { + return new LayerIterator(message) { + @Override + boolean matches(Nested layer) { + return layer instanceof EncryptedData; + } + + @Override + SessionKey getProperty(Layer last) { + return ((EncryptedData) last).getSessionKey(); + } + }; + } + public String getFilename() { return findLiteralData().getFileName(); } 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 75359de1..7e34d329 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 @@ -4,6 +4,7 @@ package org.pgpainless.decryption_verification; +import java.io.BufferedInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; @@ -12,6 +13,7 @@ import java.util.ArrayList; import java.util.Collection; import java.util.List; import java.util.Stack; +import javax.annotation.Nonnull; import org.bouncycastle.bcpg.BCPGInputStream; import org.bouncycastle.openpgp.PGPCompressedData; @@ -28,7 +30,6 @@ import org.bouncycastle.openpgp.PGPPublicKeyRing; import org.bouncycastle.openpgp.PGPSecretKey; import org.bouncycastle.openpgp.PGPSecretKeyRing; import org.bouncycastle.openpgp.PGPSignature; -import org.bouncycastle.openpgp.PGPUtil; import org.bouncycastle.openpgp.operator.PBEDataDecryptorFactory; import org.bouncycastle.openpgp.operator.PGPContentVerifierBuilderProvider; import org.bouncycastle.openpgp.operator.PublicKeyDataDecryptorFactory; @@ -46,25 +47,27 @@ import org.pgpainless.decryption_verification.automaton.StackAlphabet; import org.pgpainless.exception.MalformedOpenPgpMessageException; import org.pgpainless.exception.MessageNotIntegrityProtectedException; import org.pgpainless.exception.MissingDecryptionMethodException; +import org.pgpainless.exception.SignatureValidationException; +import org.pgpainless.exception.UnacceptableAlgorithmException; import org.pgpainless.implementation.ImplementationFactory; +import org.pgpainless.key.SubkeyIdentifier; import org.pgpainless.key.info.KeyRingInfo; import org.pgpainless.key.protection.SecretKeyRingProtector; import org.pgpainless.key.protection.UnlockSecretKey; +import org.pgpainless.policy.Policy; import org.pgpainless.signature.SignatureUtils; import org.pgpainless.util.Passphrase; +import org.pgpainless.util.SessionKey; import org.pgpainless.util.Tuple; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import javax.annotation.Nonnull; - -public class OpenPgpMessageInputStream extends InputStream { +public class OpenPgpMessageInputStream extends DecryptionStream { private static final Logger LOGGER = LoggerFactory.getLogger(OpenPgpMessageInputStream.class); // Options to consume the data protected final ConsumerOptions options; - protected final OpenPgpMetadata.Builder resultBuilder; // Pushdown Automaton to verify validity of OpenPGP packet sequence in an OpenPGP message protected final PDA automaton = new PDA(); // InputStream of OpenPGP packets @@ -76,6 +79,7 @@ public class OpenPgpMessageInputStream extends InputStream { private final Signatures signatures; private final MessageMetadata.Layer metadata; + private final Policy policy; public OpenPgpMessageInputStream(InputStream inputStream, ConsumerOptions options) throws IOException, PGPException { @@ -84,10 +88,11 @@ public class OpenPgpMessageInputStream extends InputStream { OpenPgpMessageInputStream(InputStream inputStream, ConsumerOptions options, MessageMetadata.Layer metadata) throws PGPException, IOException { + super(OpenPgpMetadata.getBuilder()); + this.policy = PGPainless.getPolicy(); this.options = options; this.metadata = metadata; - this.resultBuilder = OpenPgpMetadata.getBuilder(); this.signatures = new Signatures(options); // Add detached signatures only on the outermost OpenPgpMessageInputStream @@ -210,7 +215,8 @@ public class OpenPgpMessageInputStream extends InputStream { PGPCompressedData compressedData = packetInputStream.readCompressedData(); MessageMetadata.CompressedData compressionLayer = new MessageMetadata.CompressedData( CompressionAlgorithm.fromId(compressedData.getAlgorithm())); - nestedInputStream = new OpenPgpMessageInputStream(compressedData.getDataStream(), options, compressionLayer); + InputStream decompressed = compressedData.getDataStream(); + nestedInputStream = new OpenPgpMessageInputStream(buffer(decompressed), options, compressionLayer); } private void processLiteralData() throws IOException { @@ -232,19 +238,25 @@ public class OpenPgpMessageInputStream extends InputStream { // Try session key if (options.getSessionKey() != null) { + SessionKey sessionKey = options.getSessionKey(); + if (!policy.getSymmetricKeyDecryptionAlgorithmPolicy().isAcceptable(sessionKey.getAlgorithm())) { + throw new UnacceptableAlgorithmException("Symmetric algorithm " + sessionKey.getAlgorithm() + " is not acceptable."); + } SessionKeyDataDecryptorFactory decryptorFactory = ImplementationFactory.getInstance() - .getSessionKeyDataDecryptorFactory(options.getSessionKey()); + .getSessionKeyDataDecryptorFactory(sessionKey); // TODO: Replace with encDataList.addSessionKeyDecryptionMethod(sessionKey) PGPEncryptedData esk = esks.all().get(0); try { - MessageMetadata.EncryptedData encryptedData = new MessageMetadata.EncryptedData(options.getSessionKey().getAlgorithm()); + MessageMetadata.EncryptedData encryptedData = new MessageMetadata.EncryptedData(sessionKey.getAlgorithm()); if (esk instanceof PGPPBEEncryptedData) { PGPPBEEncryptedData skesk = (PGPPBEEncryptedData) esk; - nestedInputStream = new OpenPgpMessageInputStream(skesk.getDataStream(decryptorFactory), options, encryptedData); + InputStream decrypted = skesk.getDataStream(decryptorFactory); + nestedInputStream = new OpenPgpMessageInputStream(buffer(decrypted), options, encryptedData); return true; } else if (esk instanceof PGPPublicKeyEncryptedData) { PGPPublicKeyEncryptedData pkesk = (PGPPublicKeyEncryptedData) esk; - nestedInputStream = new OpenPgpMessageInputStream(pkesk.getDataStream(decryptorFactory), options, encryptedData); + InputStream decrypted = pkesk.getDataStream(decryptorFactory); + nestedInputStream = new OpenPgpMessageInputStream(buffer(decrypted), options, encryptedData); return true; } else { throw new RuntimeException("Unknown ESK class type: " + esk.getClass().getName()); @@ -263,7 +275,7 @@ public class OpenPgpMessageInputStream extends InputStream { InputStream decrypted = skesk.getDataStream(decryptorFactory); MessageMetadata.EncryptedData encryptedData = new MessageMetadata.EncryptedData( SymmetricKeyAlgorithm.requireFromId(skesk.getSymmetricAlgorithm(decryptorFactory))); - nestedInputStream = new OpenPgpMessageInputStream(decrypted, options, encryptedData); + nestedInputStream = new OpenPgpMessageInputStream(buffer(decrypted), options, encryptedData); return true; } catch (PGPException e) { // password mismatch? Try next password @@ -286,14 +298,20 @@ public class OpenPgpMessageInputStream extends InputStream { PublicKeyDataDecryptorFactory decryptorFactory = ImplementationFactory.getInstance() .getPublicKeyDataDecryptorFactory(privateKey); try { + SymmetricKeyAlgorithm symAlg = SymmetricKeyAlgorithm.requireFromId(pkesk.getSymmetricAlgorithm(decryptorFactory)); + if (!policy.getSymmetricKeyDecryptionAlgorithmPolicy().isAcceptable(symAlg)) { + throw new UnacceptableAlgorithmException("Symmetric-key algorithm " + symAlg + " is not acceptable."); + } InputStream decrypted = pkesk.getDataStream(decryptorFactory); MessageMetadata.EncryptedData encryptedData = new MessageMetadata.EncryptedData( SymmetricKeyAlgorithm.requireFromId(pkesk.getSymmetricAlgorithm(decryptorFactory))); - nestedInputStream = new OpenPgpMessageInputStream(PGPUtil.getDecoderStream(decrypted), options, encryptedData); + nestedInputStream = new OpenPgpMessageInputStream(buffer(decrypted), options, encryptedData); return true; } catch (PGPException e) { - // hm :/ + if (e instanceof UnacceptableAlgorithmException) { + throw e; + } } } @@ -309,7 +327,7 @@ public class OpenPgpMessageInputStream extends InputStream { InputStream decrypted = pkesk.getDataStream(decryptorFactory); MessageMetadata.EncryptedData encryptedData = new MessageMetadata.EncryptedData( SymmetricKeyAlgorithm.requireFromId(pkesk.getSymmetricAlgorithm(decryptorFactory))); - nestedInputStream = new OpenPgpMessageInputStream(decrypted, options, encryptedData); + nestedInputStream = new OpenPgpMessageInputStream(buffer(decrypted), options, encryptedData); return true; } catch (PGPException e) { // hm :/ @@ -321,6 +339,10 @@ public class OpenPgpMessageInputStream extends InputStream { return false; } + private static InputStream buffer(InputStream inputStream) { + return new BufferedInputStream(inputStream); + } + private List> findPotentialDecryptionKeys(PGPPublicKeyEncryptedData pkesk) { int algorithm = pkesk.getAlgorithm(); List> decryptionKeyCandidates = new ArrayList<>(); @@ -420,6 +442,7 @@ public class OpenPgpMessageInputStream extends InputStream { @Override public void close() throws IOException { + super.close(); if (closed) { automaton.assertValid(); return; @@ -502,6 +525,8 @@ public class OpenPgpMessageInputStream extends InputStream { final List correspondingSignatures; boolean isLiteral = true; + final List verified = new ArrayList<>(); + private Signatures(ConsumerOptions options) { this.options = options; this.detachedSignatures = new ArrayList<>(); @@ -521,19 +546,19 @@ public class OpenPgpMessageInputStream extends InputStream { long keyId = SignatureUtils.determineIssuerKeyId(signature); PGPPublicKeyRing certificate = findCertificate(keyId); initialize(signature, certificate, keyId); - this.detachedSignatures.add(new SIG(signature)); + this.detachedSignatures.add(new SIG(signature, certificate, keyId)); } void addPrependedSignature(PGPSignature signature) { long keyId = SignatureUtils.determineIssuerKeyId(signature); PGPPublicKeyRing certificate = findCertificate(keyId); initialize(signature, certificate, keyId); - this.prependedSignatures.add(new SIG(signature)); + this.prependedSignatures.add(new SIG(signature, certificate, keyId)); } void addOnePassSignature(PGPOnePassSignature signature) { PGPPublicKeyRing certificate = findCertificate(signature.getKeyID()); - OPS ops = new OPS(signature); + OPS ops = new OPS(signature, certificate, signature.getKeyID()); ops.init(certificate); onePassSignatures.add(ops); @@ -546,7 +571,7 @@ public class OpenPgpMessageInputStream extends InputStream { void addCorrespondingOnePassSignature(PGPSignature signature) { for (int i = onePassSignatures.size() - 1; i >= 0; i--) { OPS onePassSignature = onePassSignatures.get(i); - if (onePassSignature.signature.getKeyID() != signature.getKeyID()) { + if (onePassSignature.opSignature.getKeyID() != signature.getKeyID()) { continue; } if (onePassSignature.finished) { @@ -554,8 +579,8 @@ public class OpenPgpMessageInputStream extends InputStream { } boolean verified = onePassSignature.verify(signature); - log("One-Pass-Signature by " + Long.toHexString(onePassSignature.signature.getKeyID()) + " is " + (verified ? "verified" : "unverified")); - System.out.println(onePassSignature); + log("One-Pass-Signature by " + Long.toHexString(onePassSignature.opSignature.getKeyID()) + " is " + (verified ? "verified" : "unverified")); + log(onePassSignature.toString()); break; } } @@ -666,14 +691,20 @@ public class OpenPgpMessageInputStream extends InputStream { public void finish() { for (SIG detached : detachedSignatures) { boolean verified = detached.verify(); + if (verified) { + this.verified.add(detached.signature); + } log("Detached Signature by " + Long.toHexString(detached.signature.getKeyID()) + " is " + (verified ? "verified" : "unverified")); - System.out.println(detached); + log(detached.toString()); } for (SIG prepended : prependedSignatures) { boolean verified = prepended.verify(); + if (verified) { + this.verified.add(prepended.signature); + } log("Prepended Signature by " + Long.toHexString(prepended.signature.getKeyID()) + " is " + (verified ? "verified" : "unverified")); - System.out.println(prepended); + log(prepended.toString()); } } @@ -701,11 +732,15 @@ public class OpenPgpMessageInputStream extends InputStream { static class SIG { ByteArrayOutputStream bytes = new ByteArrayOutputStream(); PGPSignature signature; + PGPPublicKeyRing certificate; + long keyId; boolean finished; boolean valid; - public SIG(PGPSignature signature) { + public SIG(PGPSignature signature, PGPPublicKeyRing certificate, long keyId) { this.signature = signature; + this.certificate = certificate; + this.keyId = keyId; } public void init(PGPPublicKeyRing certificate) { @@ -713,6 +748,9 @@ public class OpenPgpMessageInputStream extends InputStream { } public boolean verify() { + if (finished) { + throw new IllegalStateException("Already finished."); + } finished = true; try { valid = this.signature.verify(); @@ -780,26 +818,32 @@ public class OpenPgpMessageInputStream extends InputStream { static class OPS { ByteArrayOutputStream bytes = new ByteArrayOutputStream(); - PGPOnePassSignature signature; + PGPOnePassSignature opSignature; + PGPSignature signature; + PGPPublicKeyRing certificate; + long keyId; boolean finished; boolean valid; - public OPS(PGPOnePassSignature signature) { - this.signature = signature; + public OPS(PGPOnePassSignature signature, PGPPublicKeyRing certificate, long keyId) { + this.opSignature = signature; + this.certificate = certificate; + this.keyId = keyId; } public void init(PGPPublicKeyRing certificate) { - initialize(signature, certificate); + initialize(opSignature, certificate); } public boolean verify(PGPSignature signature) { - if (this.signature.getKeyID() != signature.getKeyID()) { + if (this.opSignature.getKeyID() != signature.getKeyID()) { // nope return false; } + this.signature = signature; finished = true; try { - valid = this.signature.verify(signature); + valid = this.opSignature.verify(signature); } catch (PGPException e) { log("Cannot verify OPS " + signature.getKeyID()); } @@ -811,7 +855,7 @@ public class OpenPgpMessageInputStream extends InputStream { log("Updating finished sig!"); return; } - signature.update(b); + opSignature.update(b); bytes.write(b); } @@ -820,7 +864,7 @@ public class OpenPgpMessageInputStream extends InputStream { log("Updating finished sig!"); return; } - signature.update(bytes, off, len); + opSignature.update(bytes, off, len); this.bytes.write(bytes, off, len); } @@ -833,7 +877,7 @@ public class OpenPgpMessageInputStream extends InputStream { String SIG1f = "c2c13b0400010a006f058262c806350910fbfcc82a015e7330471400000000001e002073616c74406e6f746174696f6e732e736571756f69612d7067702e6f7267b0409ed8ea96dac66447bdff5b7b60c9f80a0ab91d257029153dc3b6d8c27b98162104d1a66e1a23b182c9980f788cfbfcc82a015e7330000029640c00846b5096d92474fd446cc7edaf9f14572cab93a80e12384c1e829f95debc6e8373c2ce5402be53dc1a18cf92a0ed909e0fb38855713ef8ffb13502ffac7c830fa254cc1aa6c666a97b0cc3bc176538f6913d3b8e8981a65cc42df10e0f39e4d0a06dfe961437b59a71892f4fca1116aed15123ea0d86a7b2ce47dd9d3ef22d920631bc011e82babe03ad5d72b3ba7f95bf646f20ccf6f7a4d95de37397c76c7d53741458e51ab6074007f61181c7b88b7c98f5b7510c8dfa3be01f4841501679478b15c5249d928e2a10d15ec63efa1500b994d5bfb32ffb174a976116930eb97a111e6dfd4c5e43e04a5d76ba74806a62fda63a8c3f53f6eebaf852892340e81dd08bbf348454a2cf525aeb512cf33aeeee78465ee4c442e41cc45ac4e3bb0c3333677aa60332ee7f464d9020f8554b82d619872477cca18d8431888f4ae8abe5894e9720f759c410cd7991db12703dc147040dd0d3758223e0b75de6ceae49c1a0c2c45efedeb7114ae785cc886afdc45c82172e4476e1ab5b86dc4314dd76"; String SIG2 = "c2c10400010a006f058262c806350910fbfcc82a015e7330471400000000001e002073616c74406e6f746174696f6e732e736571756f69612d7067702e6f7267a4d9c117dc7ba3a7e9270856f128d2ab271743eac3cb5750b22a89bd5fd60753162104d1a66e1a23b182c9980f788cfbfcc82a015e73300000b8400bff796c20fa8b25ff7a42686338e06417a2966e85a0fc2723c928bef6cd19d34cf5e7d55ada33080613012dadb79e0278e59d9e7ed7d2d6102912a5f768c2e75b60099225c3d8bfe0c123240188b80dbee89b9b3bd5b13ccc662abc37e2129b6968adac9aba43aa778c0fe4fe337591ee87a96a29a013debc83555293c877144fc676aa1b03782c501949521a320adf6ad96c4f2e036b52a18369c637fdc49033696a84d03a69580b953187fce5aca6fb26fc8815da9f3b513bfe8e304f33ecb4b521aeb7d09c4a284ea66123bd0d6a358b2526d762ca110e1f7f20b3038d774b64d5cfd34e2213765828359d7afc5bf24d5270e99d80c3c1568fa01624b6ea1e9ce4e6890ce9bacf6611a45d41e2671f68f5b096446bf08d27ce75608425b2e3ab92146229ad1fcd8224aca5b5f73960506e7df07bfbf3664348e8ecbfb2eb467b9cfe412cb377a6ee2eb5fd11be9cf9208fe9a74c296f52cfa02a1eb0519ad9a8349bf6ccd6495feb7e391451bf96e08a0798883dee5974e47cbf3b51f111b6d3"; String SIG2f = "c2c13b0400010a006f058262c806350910fbfcc82a015e7330471400000000001e002073616c74406e6f746174696f6e732e736571756f69612d7067702e6f7267a4d9c117dc7ba3a7e9270856f128d2ab271743eac3cb5750b22a89bd5fd60753162104d1a66e1a23b182c9980f788cfbfcc82a015e73300000b8400bff796c20fa8b25ff7a42686338e06417a2966e85a0fc2723c928bef6cd19d34cf5e7d55ada33080613012dadb79e0278e59d9e7ed7d2d6102912a5f768c2e75b60099225c3d8bfe0c123240188b80dbee89b9b3bd5b13ccc662abc37e2129b6968adac9aba43aa778c0fe4fe337591ee87a96a29a013debc83555293c877144fc676aa1b03782c501949521a320adf6ad96c4f2e036b52a18369c637fdc49033696a84d03a69580b953187fce5aca6fb26fc8815da9f3b513bfe8e304f33ecb4b521aeb7d09c4a284ea66123bd0d6a358b2526d762ca110e1f7f20b3038d774b64d5cfd34e2213765828359d7afc5bf24d5270e99d80c3c1568fa01624b6ea1e9ce4e6890ce9bacf6611a45d41e2671f68f5b096446bf08d27ce75608425b2e3ab92146229ad1fcd8224aca5b5f73960506e7df07bfbf3664348e8ecbfb2eb467b9cfe412cb377a6ee2eb5fd11be9cf9208fe9a74c296f52cfa02a1eb0519ad9a8349bf6ccd6495feb7e391451bf96e08a0798883dee5974e47cbf3b51f111b6d3"; - String out = "last=" + signature.isContaining() + "\n"; + String out = "last=" + opSignature.isContaining() + "\n"; String hex = Hex.toHexString(bytes.toByteArray()); while (hex.contains(OPS)) { @@ -863,10 +907,64 @@ public class OpenPgpMessageInputStream extends InputStream { } } + @Override + public OpenPgpMetadata getResult() { + MessageMetadata m = getMetadata(); + resultBuilder.setCompressionAlgorithm(m.getCompressionAlgorithm()); + resultBuilder.setModificationDate(m.getModificationDate()); + resultBuilder.setFileName(m.getFilename()); + resultBuilder.setFileEncoding(m.getFormat()); + resultBuilder.setSessionKey(m.getSessionKey()); + + for (Signatures.OPS ops : signatures.onePassSignatures) { + if (!ops.finished) { + continue; + } + + SubkeyIdentifier identifier = new SubkeyIdentifier(ops.certificate, ops.keyId); + SignatureVerification verification = new SignatureVerification(ops.signature, identifier); + if (ops.valid) { + resultBuilder.addVerifiedInbandSignature(verification); + } else { + resultBuilder.addInvalidInbandSignature(verification, new SignatureValidationException("Incorrect signature.")); + } + } + + for (Signatures.SIG prep : signatures.prependedSignatures) { + if (!prep.finished) { + continue; + } + + SubkeyIdentifier identifier = new SubkeyIdentifier(prep.certificate, prep.keyId); + SignatureVerification verification = new SignatureVerification(prep.signature, identifier); + if (prep.valid) { + resultBuilder.addVerifiedInbandSignature(verification); + } else { + resultBuilder.addInvalidInbandSignature(verification, new SignatureValidationException("Incorrect signature.")); + } + } + + for (Signatures.SIG det : signatures.detachedSignatures) { + if (!det.finished) { + continue; + } + + SubkeyIdentifier identifier = new SubkeyIdentifier(det.certificate, det.keyId); + SignatureVerification verification = new SignatureVerification(det.signature, identifier); + if (det.valid) { + resultBuilder.addVerifiedDetachedSignature(verification); + } else { + resultBuilder.addInvalidDetachedSignature(verification, new SignatureValidationException("Incorrect signature.")); + } + } + + return resultBuilder.build(); + } + static void log(String message) { LOGGER.debug(message); // CHECKSTYLE:OFF - System.out.println(message); + // System.out.println(message); // CHECKSTYLE:ON } diff --git a/pgpainless-core/src/test/java/investigations/InvestigateMultiSEIPMessageHandlingTest.java b/pgpainless-core/src/test/java/investigations/InvestigateMultiSEIPMessageHandlingTest.java index 3350203a..d31a0e0f 100644 --- a/pgpainless-core/src/test/java/investigations/InvestigateMultiSEIPMessageHandlingTest.java +++ b/pgpainless-core/src/test/java/investigations/InvestigateMultiSEIPMessageHandlingTest.java @@ -6,6 +6,7 @@ package investigations; 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 java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; @@ -32,9 +33,8 @@ import org.pgpainless.algorithm.SignatureType; import org.pgpainless.algorithm.SymmetricKeyAlgorithm; import org.pgpainless.decryption_verification.ConsumerOptions; import org.pgpainless.decryption_verification.DecryptionStream; -import org.pgpainless.decryption_verification.OpenPgpMetadata; +import org.pgpainless.exception.MalformedOpenPgpMessageException; import org.pgpainless.implementation.ImplementationFactory; -import org.pgpainless.key.SubkeyIdentifier; import org.pgpainless.key.info.KeyRingInfo; import org.pgpainless.key.protection.UnlockSecretKey; import org.pgpainless.util.Passphrase; @@ -177,7 +177,7 @@ public class InvestigateMultiSEIPMessageHandlingTest { } @Test - public void testDecryptAndVerifyDoesIgnoreAppendedSEIPData() throws IOException, PGPException { + public void testDecryptAndVerifyDetectsAppendedSEIPData() throws IOException, PGPException { PGPSecretKeyRing ring1 = PGPainless.readKeyRing().secretKeyRing(KEY1); PGPSecretKeyRing ring2 = PGPainless.readKeyRing().secretKeyRing(KEY2); @@ -191,15 +191,6 @@ public class InvestigateMultiSEIPMessageHandlingTest { .withOptions(options); ByteArrayOutputStream out = new ByteArrayOutputStream(); - Streams.pipeAll(decryptionStream, out); - decryptionStream.close(); - - assertArrayEquals(data1.getBytes(StandardCharsets.UTF_8), out.toByteArray()); - OpenPgpMetadata metadata = decryptionStream.getResult(); - assertEquals(1, metadata.getVerifiedSignatures().size(), - "The first SEIP packet is signed exactly only by the signing key of ring1."); - assertEquals( - new SubkeyIdentifier(ring1, new KeyRingInfo(ring1).getSigningSubkeys().get(0).getKeyID()), - metadata.getVerifiedSignatures().keySet().iterator().next()); + assertThrows(MalformedOpenPgpMessageException.class, () -> Streams.pipeAll(decryptionStream, out)); } } diff --git a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/PreventDecryptionUsingNonEncryptionKeyTest.java b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/PreventDecryptionUsingNonEncryptionKeyTest.java index 851a1bef..7474301b 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/PreventDecryptionUsingNonEncryptionKeyTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/PreventDecryptionUsingNonEncryptionKeyTest.java @@ -8,6 +8,8 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.EOFException; import java.io.IOException; import java.nio.charset.StandardCharsets; @@ -185,7 +187,7 @@ public class PreventDecryptionUsingNonEncryptionKeyTest { decryptionStream.close(); OpenPgpMetadata metadata = decryptionStream.getResult(); - assertEquals(metadata.getDecryptionKey(), new SubkeyIdentifier(secretKeys, secretKeys.getPublicKey().getKeyID())); + assertEquals(new SubkeyIdentifier(secretKeys, secretKeys.getPublicKey().getKeyID()), metadata.getDecryptionKey()); } @Test From 78e607ab85ca1a6109b0fa6d2d3216d23aee7529 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Sun, 16 Oct 2022 18:15:31 +0200 Subject: [PATCH 27/80] Cleaning up and collect signature verifications --- .../MessageMetadata.java | 85 +++- .../OpenPgpMessageInputStream.java | 375 ++++++++---------- .../OpenPgpMessageInputStreamTest.java | 19 + 3 files changed, 262 insertions(+), 217 deletions(-) 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 index 87f6d7f6..bb2f5c76 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/MessageMetadata.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/MessageMetadata.java @@ -91,6 +91,62 @@ public class MessageMetadata { }; } + public @Nonnull List getVerifiedSignatures() { + List verifications = new ArrayList<>(); + Iterator> verificationsByLayer = getVerifiedSignaturesByLayer(); + while (verificationsByLayer.hasNext()) { + verifications.addAll(verificationsByLayer.next()); + } + return verifications; + } + + public @Nonnull Iterator> getVerifiedSignaturesByLayer() { + return new LayerIterator>(message) { + @Override + boolean matches(Nested layer) { + return layer instanceof Layer; + } + + @Override + boolean matches(Layer layer) { + return true; + } + + @Override + List getProperty(Layer last) { + return new ArrayList<>(last.getVerifiedSignatures()); + } + }; + } + + public @Nonnull List getRejectedSignatures() { + List rejected = new ArrayList<>(); + Iterator> rejectedByLayer = getRejectedSignaturesByLayer(); + while (rejectedByLayer.hasNext()) { + rejected.addAll(rejectedByLayer.next()); + } + return rejected; + } + + public @Nonnull Iterator> getRejectedSignaturesByLayer() { + return new LayerIterator>(message) { + @Override + boolean matches(Nested layer) { + return layer instanceof Layer; + } + + @Override + boolean matches(Layer layer) { + return true; + } + + @Override + List getProperty(Layer last) { + return new ArrayList<>(last.getFailedSignatures()); + } + }; + } + public String getFilename() { return findLiteralData().getFileName(); } @@ -132,6 +188,14 @@ public class MessageMetadata { public List getFailedSignatures() { return new ArrayList<>(failedSignatures); } + + void addVerifiedSignature(SignatureVerification signatureVerification) { + verifiedSignatures.add(signatureVerification); + } + + void addFailedSignature(SignatureVerification.Failure failure) { + failedSignatures.add(failure); + } } public interface Nested { @@ -223,9 +287,11 @@ public class MessageMetadata { private abstract static class LayerIterator implements Iterator { private Nested current; Layer last = null; + Message parent; LayerIterator(Message message) { super(); + this.parent = message; this.current = message.child; if (matches(current)) { last = (Layer) current; @@ -234,6 +300,9 @@ public class MessageMetadata { @Override public boolean hasNext() { + if (parent != null && matches(parent)) { + return true; + } if (last == null) { findNext(); } @@ -242,6 +311,11 @@ public class MessageMetadata { @Override public O next() { + if (parent != null && matches(parent)) { + O property = getProperty(parent); + parent = null; + return property; + } if (last == null) { findNext(); } @@ -263,7 +337,16 @@ public class MessageMetadata { } } - abstract boolean matches(Nested layer); + boolean matches(Nested layer) { + return false; + } + + boolean matches(Layer layer) { + if (layer instanceof Nested) { + return matches((Nested) layer); + } + return false; + } 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 7e34d329..9e26dc20 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 @@ -34,7 +34,6 @@ 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.util.encoders.Hex; import org.pgpainless.PGPainless; import org.pgpainless.algorithm.CompressionAlgorithm; import org.pgpainless.algorithm.EncryptionPurpose; @@ -81,16 +80,27 @@ public class OpenPgpMessageInputStream extends DecryptionStream { private final MessageMetadata.Layer metadata; private final Policy policy; - public OpenPgpMessageInputStream(InputStream inputStream, ConsumerOptions options) + public OpenPgpMessageInputStream(@Nonnull InputStream inputStream, + @Nonnull ConsumerOptions options) throws IOException, PGPException { - this(inputStream, options, new MessageMetadata.Message()); + this(inputStream, options, PGPainless.getPolicy()); } - OpenPgpMessageInputStream(InputStream inputStream, ConsumerOptions options, MessageMetadata.Layer metadata) + public OpenPgpMessageInputStream(@Nonnull InputStream inputStream, + @Nonnull ConsumerOptions options, + @Nonnull Policy policy) + throws PGPException, IOException { + this(inputStream, options, new MessageMetadata.Message(), policy); + } + + protected OpenPgpMessageInputStream(@Nonnull InputStream inputStream, + @Nonnull ConsumerOptions options, + @Nonnull MessageMetadata.Layer metadata, + @Nonnull Policy policy) throws PGPException, IOException { super(OpenPgpMetadata.getBuilder()); - this.policy = PGPainless.getPolicy(); + this.policy = policy; this.options = options; this.metadata = metadata; this.signatures = new Signatures(options); @@ -100,6 +110,7 @@ public class OpenPgpMessageInputStream extends DecryptionStream { this.signatures.addDetachedSignatures(options.getDetachedSignatures()); } + // tee out packet bytes for signature verification packetInputStream = new TeeBCPGInputStream(BCPGInputStream.wrap(inputStream), signatures); // *omnomnom* @@ -125,37 +136,30 @@ public class OpenPgpMessageInputStream extends DecryptionStream { private void consumePackets() throws IOException, PGPException { OpenPgpPacket nextPacket; - loop: while ((nextPacket = packetInputStream.nextPacketTag()) != null) { + + loop: // we break this when we go deeper. + while ((nextPacket = packetInputStream.nextPacketTag()) != null) { signatures.nextPacket(nextPacket); switch (nextPacket) { // Literal Data - the literal data content is the new input stream case LIT: - automaton.next(InputAlphabet.LiteralData); processLiteralData(); break loop; // Compressed Data - the content contains another OpenPGP message case COMP: - automaton.next(InputAlphabet.CompressedData); processCompressedData(); break loop; // One Pass Signature case OPS: - automaton.next(InputAlphabet.OnePassSignature); - PGPOnePassSignature onePassSignature = readOnePassSignature(); - signatures.addOnePassSignature(onePassSignature); + processOnePassSignature(); break; // Signature - either prepended to the message, or corresponding to a One Pass Signature case SIG: - // true if Signature corresponds to OnePassSignature - boolean isSigForOPS = automaton.peekStack() == StackAlphabet.ops; - automaton.next(InputAlphabet.Signature); - - processSignature(isSigForOPS); - + processSignature(); break; // Encrypted Data (ESKs and SED/SEIPD are parsed the same by BC) @@ -163,14 +167,13 @@ public class OpenPgpMessageInputStream extends DecryptionStream { case SKESK: case SED: case SEIPD: - automaton.next(InputAlphabet.EncryptedData); if (processEncryptedData()) { break loop; } throw new MissingDecryptionMethodException("No working decryption method found."); - // Marker Packets need to be skipped and ignored + // Marker Packets need to be skipped and ignored case MARKER: packetInputStream.readMarker(); break; @@ -200,36 +203,51 @@ public class OpenPgpMessageInputStream extends DecryptionStream { } } - private void processSignature(boolean isSigForOPS) throws PGPException, IOException { - PGPSignature signature = readSignature(); - if (isSigForOPS) { - signatures.leaveNesting(); // TODO: Only leave nesting if all OPSs are dealt with - signatures.addCorrespondingOnePassSignature(signature); - } else { - signatures.addPrependedSignature(signature); - } + private void processLiteralData() throws IOException { + automaton.next(InputAlphabet.LiteralData); + PGPLiteralData literalData = packetInputStream.readLiteralData(); + this.metadata.setChild(new MessageMetadata.LiteralData( + literalData.getFileName(), + literalData.getModificationTime(), + StreamEncoding.requireFromCode(literalData.getFormat()))); + nestedInputStream = literalData.getDataStream(); } private void processCompressedData() throws IOException, PGPException { + automaton.next(InputAlphabet.CompressedData); signatures.enterNesting(); PGPCompressedData compressedData = packetInputStream.readCompressedData(); MessageMetadata.CompressedData compressionLayer = new MessageMetadata.CompressedData( CompressionAlgorithm.fromId(compressedData.getAlgorithm())); InputStream decompressed = compressedData.getDataStream(); - nestedInputStream = new OpenPgpMessageInputStream(buffer(decompressed), options, compressionLayer); + nestedInputStream = new OpenPgpMessageInputStream(buffer(decompressed), options, compressionLayer, policy); } - private void processLiteralData() throws IOException { - PGPLiteralData literalData = packetInputStream.readLiteralData(); - this.metadata.setChild(new MessageMetadata.LiteralData(literalData.getFileName(), literalData.getModificationTime(), - StreamEncoding.requireFromCode(literalData.getFormat()))); - nestedInputStream = literalData.getDataStream(); + private void processOnePassSignature() throws PGPException, IOException { + automaton.next(InputAlphabet.OnePassSignature); + PGPOnePassSignature onePassSignature = packetInputStream.readOnePassSignature(); + signatures.addOnePassSignature(onePassSignature); + } + + private void processSignature() throws PGPException, IOException { + // true if Signature corresponds to OnePassSignature + boolean isSigForOPS = automaton.peekStack() == StackAlphabet.ops; + automaton.next(InputAlphabet.Signature); + PGPSignature signature = packetInputStream.readSignature(); + if (isSigForOPS) { + signatures.leaveNesting(); // TODO: Only leave nesting if all OPSs of the nesting layer are dealt with + signatures.addCorrespondingOnePassSignature(signature, metadata); + } else { + signatures.addPrependedSignature(signature); + } } private boolean processEncryptedData() throws IOException, PGPException { + automaton.next(InputAlphabet.EncryptedData); PGPEncryptedDataList encDataList = packetInputStream.readEncryptedDataList(); // TODO: Replace with !encDataList.isIntegrityProtected() + // once BC ships it if (!encDataList.get(0).isIntegrityProtected()) { throw new MessageNotIntegrityProtectedException(); } @@ -239,24 +257,25 @@ public class OpenPgpMessageInputStream extends DecryptionStream { // Try session key if (options.getSessionKey() != null) { SessionKey sessionKey = options.getSessionKey(); - if (!policy.getSymmetricKeyDecryptionAlgorithmPolicy().isAcceptable(sessionKey.getAlgorithm())) { - throw new UnacceptableAlgorithmException("Symmetric algorithm " + sessionKey.getAlgorithm() + " is not acceptable."); - } + + throwIfUnacceptable(sessionKey.getAlgorithm()); + SessionKeyDataDecryptorFactory decryptorFactory = ImplementationFactory.getInstance() .getSessionKeyDataDecryptorFactory(sessionKey); - // TODO: Replace with encDataList.addSessionKeyDecryptionMethod(sessionKey) - PGPEncryptedData esk = esks.all().get(0); + MessageMetadata.EncryptedData encryptedData = new MessageMetadata.EncryptedData(sessionKey.getAlgorithm()); + try { - MessageMetadata.EncryptedData encryptedData = new MessageMetadata.EncryptedData(sessionKey.getAlgorithm()); + // TODO: Use BCs new API once shipped + PGPEncryptedData esk = esks.all().get(0); if (esk instanceof PGPPBEEncryptedData) { PGPPBEEncryptedData skesk = (PGPPBEEncryptedData) esk; InputStream decrypted = skesk.getDataStream(decryptorFactory); - nestedInputStream = new OpenPgpMessageInputStream(buffer(decrypted), options, encryptedData); + nestedInputStream = new OpenPgpMessageInputStream(buffer(decrypted), options, encryptedData, policy); return true; } else if (esk instanceof PGPPublicKeyEncryptedData) { PGPPublicKeyEncryptedData pkesk = (PGPPublicKeyEncryptedData) esk; InputStream decrypted = pkesk.getDataStream(decryptorFactory); - nestedInputStream = new OpenPgpMessageInputStream(buffer(decrypted), options, encryptedData); + nestedInputStream = new OpenPgpMessageInputStream(buffer(decrypted), options, encryptedData, policy); return true; } else { throw new RuntimeException("Unknown ESK class type: " + esk.getClass().getName()); @@ -268,19 +287,25 @@ public class OpenPgpMessageInputStream extends DecryptionStream { // Try passwords for (PGPPBEEncryptedData skesk : esks.skesks) { + SymmetricKeyAlgorithm kekAlgorithm = SymmetricKeyAlgorithm.requireFromId(skesk.getAlgorithm()); + throwIfUnacceptable(kekAlgorithm); for (Passphrase passphrase : options.getDecryptionPassphrases()) { - PBEDataDecryptorFactory decryptorFactory = ImplementationFactory.getInstance() - .getPBEDataDecryptorFactory(passphrase); - try { - InputStream decrypted = skesk.getDataStream(decryptorFactory); - MessageMetadata.EncryptedData encryptedData = new MessageMetadata.EncryptedData( - SymmetricKeyAlgorithm.requireFromId(skesk.getSymmetricAlgorithm(decryptorFactory))); - nestedInputStream = new OpenPgpMessageInputStream(buffer(decrypted), options, encryptedData); - return true; - } catch (PGPException e) { - // password mismatch? Try next password - } + PBEDataDecryptorFactory decryptorFactory = ImplementationFactory.getInstance() + .getPBEDataDecryptorFactory(passphrase); + try { + InputStream decrypted = skesk.getDataStream(decryptorFactory); + SymmetricKeyAlgorithm sessionKeyAlgorithm = SymmetricKeyAlgorithm.requireFromId( + skesk.getSymmetricAlgorithm(decryptorFactory)); + throwIfUnacceptable(sessionKeyAlgorithm); + MessageMetadata.EncryptedData encryptedData = new MessageMetadata.EncryptedData(sessionKeyAlgorithm); + nestedInputStream = new OpenPgpMessageInputStream(buffer(decrypted), options, encryptedData, policy); + return true; + } catch (UnacceptableAlgorithmException e) { + throw e; + } catch (PGPException e) { + // Password mismatch? + } } } @@ -299,19 +324,17 @@ public class OpenPgpMessageInputStream extends DecryptionStream { .getPublicKeyDataDecryptorFactory(privateKey); try { SymmetricKeyAlgorithm symAlg = SymmetricKeyAlgorithm.requireFromId(pkesk.getSymmetricAlgorithm(decryptorFactory)); - if (!policy.getSymmetricKeyDecryptionAlgorithmPolicy().isAcceptable(symAlg)) { - throw new UnacceptableAlgorithmException("Symmetric-key algorithm " + symAlg + " is not acceptable."); - } + throwIfUnacceptable(symAlg); InputStream decrypted = pkesk.getDataStream(decryptorFactory); MessageMetadata.EncryptedData encryptedData = new MessageMetadata.EncryptedData( SymmetricKeyAlgorithm.requireFromId(pkesk.getSymmetricAlgorithm(decryptorFactory))); - nestedInputStream = new OpenPgpMessageInputStream(buffer(decrypted), options, encryptedData); + nestedInputStream = new OpenPgpMessageInputStream(buffer(decrypted), options, encryptedData, policy); return true; + } catch (UnacceptableAlgorithmException e) { + throw e; } catch (PGPException e) { - if (e instanceof UnacceptableAlgorithmException) { - throw e; - } + } } @@ -327,7 +350,7 @@ public class OpenPgpMessageInputStream extends DecryptionStream { InputStream decrypted = pkesk.getDataStream(decryptorFactory); MessageMetadata.EncryptedData encryptedData = new MessageMetadata.EncryptedData( SymmetricKeyAlgorithm.requireFromId(pkesk.getSymmetricAlgorithm(decryptorFactory))); - nestedInputStream = new OpenPgpMessageInputStream(buffer(decrypted), options, encryptedData); + nestedInputStream = new OpenPgpMessageInputStream(buffer(decrypted), options, encryptedData, policy); return true; } catch (PGPException e) { // hm :/ @@ -339,6 +362,13 @@ public class OpenPgpMessageInputStream extends DecryptionStream { return false; } + private void throwIfUnacceptable(SymmetricKeyAlgorithm algorithm) + throws UnacceptableAlgorithmException { + if (!policy.getSymmetricKeyDecryptionAlgorithmPolicy().isAcceptable(algorithm)) { + throw new UnacceptableAlgorithmException("Symmetric-Key algorithm " + algorithm + " is not acceptable for message decryption."); + } + } + private static InputStream buffer(InputStream inputStream) { return new BufferedInputStream(inputStream); } @@ -370,16 +400,6 @@ public class OpenPgpMessageInputStream extends DecryptionStream { return null; } - private PGPOnePassSignature readOnePassSignature() - throws PGPException, IOException { - return packetInputStream.readOnePassSignature(); - } - - private PGPSignature readSignature() - throws PGPException, IOException { - return packetInputStream.readSignature(); - } - @Override public int read() throws IOException { if (nestedInputStream == null) { @@ -407,7 +427,7 @@ public class OpenPgpMessageInputStream extends DecryptionStream { } catch (PGPException e) { throw new RuntimeException(e); } - signatures.finish(); + signatures.finish(metadata); } return r; } @@ -435,7 +455,7 @@ public class OpenPgpMessageInputStream extends DecryptionStream { } catch (PGPException e) { throw new RuntimeException(e); } - signatures.finish(); + signatures.finish(metadata); } return r; } @@ -517,11 +537,11 @@ public class OpenPgpMessageInputStream extends DecryptionStream { // Furthermore, For 'OPS COMP(LIT("Foo")) SIG', the signature is updated with "Foo". CHAOS!!! private static final class Signatures extends OutputStream { final ConsumerOptions options; - final List detachedSignatures; - final List prependedSignatures; - final List onePassSignatures; - final Stack> opsUpdateStack; - List literalOPS = new ArrayList<>(); + final List detachedSignatures; + final List prependedSignatures; + final List onePassSignatures; + final Stack> opsUpdateStack; + List literalOPS = new ArrayList<>(); final List correspondingSignatures; boolean isLiteral = true; @@ -546,19 +566,19 @@ public class OpenPgpMessageInputStream extends DecryptionStream { long keyId = SignatureUtils.determineIssuerKeyId(signature); PGPPublicKeyRing certificate = findCertificate(keyId); initialize(signature, certificate, keyId); - this.detachedSignatures.add(new SIG(signature, certificate, keyId)); + this.detachedSignatures.add(new DetachedOrPrependedSignature(signature, certificate, keyId)); } void addPrependedSignature(PGPSignature signature) { long keyId = SignatureUtils.determineIssuerKeyId(signature); PGPPublicKeyRing certificate = findCertificate(keyId); initialize(signature, certificate, keyId); - this.prependedSignatures.add(new SIG(signature, certificate, keyId)); + this.prependedSignatures.add(new DetachedOrPrependedSignature(signature, certificate, keyId)); } void addOnePassSignature(PGPOnePassSignature signature) { PGPPublicKeyRing certificate = findCertificate(signature.getKeyID()); - OPS ops = new OPS(signature, certificate, signature.getKeyID()); + OnePassSignature ops = new OnePassSignature(signature, certificate, signature.getKeyID()); ops.init(certificate); onePassSignatures.add(ops); @@ -568,9 +588,9 @@ public class OpenPgpMessageInputStream extends DecryptionStream { } } - void addCorrespondingOnePassSignature(PGPSignature signature) { + void addCorrespondingOnePassSignature(PGPSignature signature, MessageMetadata.Layer layer) { for (int i = onePassSignatures.size() - 1; i >= 0; i--) { - OPS onePassSignature = onePassSignatures.get(i); + OnePassSignature onePassSignature = onePassSignatures.get(i); if (onePassSignature.opSignature.getKeyID() != signature.getKeyID()) { continue; } @@ -579,8 +599,14 @@ public class OpenPgpMessageInputStream extends DecryptionStream { } boolean verified = onePassSignature.verify(signature); - log("One-Pass-Signature by " + Long.toHexString(onePassSignature.opSignature.getKeyID()) + " is " + (verified ? "verified" : "unverified")); - log(onePassSignature.toString()); + SignatureVerification verification = new SignatureVerification(signature, + new SubkeyIdentifier(onePassSignature.certificate, onePassSignature.keyId)); + if (verified) { + layer.addVerifiedSignature(verification); + } else { + layer.addFailedSignature(new SignatureVerification.Failure(verification, + new SignatureValidationException("Incorrect Signature."))); + } break; } } @@ -597,11 +623,7 @@ public class OpenPgpMessageInputStream extends DecryptionStream { opsUpdateStack.pop(); } - private static void initialize(PGPSignature signature, PGPPublicKeyRing certificate, long keyId) { - if (certificate == null) { - // SHIT - return; - } + private static void initialize(@Nonnull PGPSignature signature, @Nonnull PGPPublicKeyRing certificate, long keyId) { PGPContentVerifierBuilderProvider verifierProvider = ImplementationFactory.getInstance() .getPGPContentVerifierBuilderProvider(); try { @@ -611,10 +633,7 @@ public class OpenPgpMessageInputStream extends DecryptionStream { } } - private static void initialize(PGPOnePassSignature ops, PGPPublicKeyRing certificate) { - if (certificate == null) { - return; - } + private static void initialize(@Nonnull PGPOnePassSignature ops, @Nonnull PGPPublicKeyRing certificate) { PGPContentVerifierBuilderProvider verifierProvider = ImplementationFactory.getInstance() .getPGPContentVerifierBuilderProvider(); try { @@ -635,76 +654,74 @@ public class OpenPgpMessageInputStream extends DecryptionStream { } public void updateLiteral(byte b) { - for (OPS ops : literalOPS) { + for (OnePassSignature ops : literalOPS) { ops.update(b); } - for (SIG detached : detachedSignatures) { + for (DetachedOrPrependedSignature detached : detachedSignatures) { detached.update(b); } + + for (DetachedOrPrependedSignature prepended : prependedSignatures) { + prepended.update(b); + } } public void updateLiteral(byte[] b, int off, int len) { - for (OPS ops : literalOPS) { + for (OnePassSignature ops : literalOPS) { ops.update(b, off, len); } - for (SIG detached : detachedSignatures) { + for (DetachedOrPrependedSignature detached : detachedSignatures) { detached.update(b, off, len); } + + for (DetachedOrPrependedSignature prepended : prependedSignatures) { + prepended.update(b, off, len); + } } public void updatePacket(byte b) { - for (SIG detached : detachedSignatures) { - detached.update(b); - } - - for (SIG prepended : prependedSignatures) { - prepended.update(b); - } - for (int i = opsUpdateStack.size() - 1; i >= 0; i--) { - List nestedOPSs = opsUpdateStack.get(i); - for (OPS ops : nestedOPSs) { + List nestedOPSs = opsUpdateStack.get(i); + for (OnePassSignature ops : nestedOPSs) { ops.update(b); } } } public void updatePacket(byte[] buf, int off, int len) { - for (SIG detached : detachedSignatures) { - detached.update(buf, off, len); - } - - for (SIG prepended : prependedSignatures) { - prepended.update(buf, off, len); - } - for (int i = opsUpdateStack.size() - 1; i >= 0; i--) { - List nestedOPSs = opsUpdateStack.get(i); - for (OPS ops : nestedOPSs) { + List nestedOPSs = opsUpdateStack.get(i); + for (OnePassSignature ops : nestedOPSs) { ops.update(buf, off, len); } } } - public void finish() { - for (SIG detached : detachedSignatures) { + public void finish(MessageMetadata.Layer layer) { + for (DetachedOrPrependedSignature detached : detachedSignatures) { boolean verified = detached.verify(); + SignatureVerification verification = new SignatureVerification( + detached.signature, new SubkeyIdentifier(detached.certificate, detached.keyId)); if (verified) { - this.verified.add(detached.signature); + layer.addVerifiedSignature(verification); + } else { + layer.addFailedSignature(new SignatureVerification.Failure( + verification, new SignatureValidationException("Incorrect Signature."))); } - log("Detached Signature by " + Long.toHexString(detached.signature.getKeyID()) + " is " + (verified ? "verified" : "unverified")); - log(detached.toString()); } - for (SIG prepended : prependedSignatures) { + for (DetachedOrPrependedSignature prepended : prependedSignatures) { boolean verified = prepended.verify(); + SignatureVerification verification = new SignatureVerification( + prepended.signature, new SubkeyIdentifier(prepended.certificate, prepended.keyId)); if (verified) { - this.verified.add(prepended.signature); + layer.addVerifiedSignature(verification); + } else { + layer.addFailedSignature(new SignatureVerification.Failure( + verification, new SignatureValidationException("Incorrect Signature."))); } - log("Prepended Signature by " + Long.toHexString(prepended.signature.getKeyID()) + " is " + (verified ? "verified" : "unverified")); - log(prepended.toString()); } } @@ -729,7 +746,7 @@ public class OpenPgpMessageInputStream extends DecryptionStream { } } - static class SIG { + static class DetachedOrPrependedSignature { ByteArrayOutputStream bytes = new ByteArrayOutputStream(); PGPSignature signature; PGPPublicKeyRing certificate; @@ -737,7 +754,7 @@ public class OpenPgpMessageInputStream extends DecryptionStream { boolean finished; boolean valid; - public SIG(PGPSignature signature, PGPPublicKeyRing certificate, long keyId) { + public DetachedOrPrependedSignature(PGPSignature signature, PGPPublicKeyRing certificate, long keyId) { this.signature = signature; this.certificate = certificate; this.keyId = keyId; @@ -762,8 +779,7 @@ public class OpenPgpMessageInputStream extends DecryptionStream { public void update(byte b) { if (finished) { - log("Updating finished sig!"); - return; + throw new IllegalStateException("Already finished."); } signature.update(b); bytes.write(b); @@ -771,52 +787,14 @@ public class OpenPgpMessageInputStream extends DecryptionStream { public void update(byte[] bytes, int off, int len) { if (finished) { - log("Updating finished sig!"); - return; + throw new IllegalStateException("Already finished."); } signature.update(bytes, off, len); this.bytes.write(bytes, off, len); } - - @Override - public String toString() { - String OPS = "c40d03000a01fbfcc82a015e733001"; - String LIT_H = "cb28620000000000"; - String LIT = "656e637279707420e28898207369676e20e28898207369676e20e28898207369676e"; - String SIG1 = "c2c10400010a006f058262c806350910fbfcc82a015e7330471400000000001e002073616c74406e6f746174696f6e732e736571756f69612d7067702e6f7267b0409ed8ea96dac66447bdff5b7b60c9f80a0ab91d257029153dc3b6d8c27b98162104d1a66e1a23b182c9980f788cfbfcc82a015e7330000029640c00846b5096d92474fd446cc7edaf9f14572cab93a80e12384c1e829f95debc6e8373c2ce5402be53dc1a18cf92a0ed909e0fb38855713ef8ffb13502ffac7c830fa254cc1aa6c666a97b0cc3bc176538f6913d3b8e8981a65cc42df10e0f39e4d0a06dfe961437b59a71892f4fca1116aed15123ea0d86a7b2ce47dd9d3ef22d920631bc011e82babe03ad5d72b3ba7f95bf646f20ccf6f7a4d95de37397c76c7d53741458e51ab6074007f61181c7b88b7c98f5b7510c8dfa3be01f4841501679478b15c5249d928e2a10d15ec63efa1500b994d5bfb32ffb174a976116930eb97a111e6dfd4c5e43e04a5d76ba74806a62fda63a8c3f53f6eebaf852892340e81dd08bbf348454a2cf525aeb512cf33aeeee78465ee4c442e41cc45ac4e3bb0c3333677aa60332ee7f464d9020f8554b82d619872477cca18d8431888f4ae8abe5894e9720f759c410cd7991db12703dc147040dd0d3758223e0b75de6ceae49c1a0c2c45efedeb7114ae785cc886afdc45c82172e4476e1ab5b86dc4314dd76"; - String SIG1f = "c2c13b0400010a006f058262c806350910fbfcc82a015e7330471400000000001e002073616c74406e6f746174696f6e732e736571756f69612d7067702e6f7267b0409ed8ea96dac66447bdff5b7b60c9f80a0ab91d257029153dc3b6d8c27b98162104d1a66e1a23b182c9980f788cfbfcc82a015e7330000029640c00846b5096d92474fd446cc7edaf9f14572cab93a80e12384c1e829f95debc6e8373c2ce5402be53dc1a18cf92a0ed909e0fb38855713ef8ffb13502ffac7c830fa254cc1aa6c666a97b0cc3bc176538f6913d3b8e8981a65cc42df10e0f39e4d0a06dfe961437b59a71892f4fca1116aed15123ea0d86a7b2ce47dd9d3ef22d920631bc011e82babe03ad5d72b3ba7f95bf646f20ccf6f7a4d95de37397c76c7d53741458e51ab6074007f61181c7b88b7c98f5b7510c8dfa3be01f4841501679478b15c5249d928e2a10d15ec63efa1500b994d5bfb32ffb174a976116930eb97a111e6dfd4c5e43e04a5d76ba74806a62fda63a8c3f53f6eebaf852892340e81dd08bbf348454a2cf525aeb512cf33aeeee78465ee4c442e41cc45ac4e3bb0c3333677aa60332ee7f464d9020f8554b82d619872477cca18d8431888f4ae8abe5894e9720f759c410cd7991db12703dc147040dd0d3758223e0b75de6ceae49c1a0c2c45efedeb7114ae785cc886afdc45c82172e4476e1ab5b86dc4314dd76"; - String SIG2 = "c2c10400010a006f058262c806350910fbfcc82a015e7330471400000000001e002073616c74406e6f746174696f6e732e736571756f69612d7067702e6f7267a4d9c117dc7ba3a7e9270856f128d2ab271743eac3cb5750b22a89bd5fd60753162104d1a66e1a23b182c9980f788cfbfcc82a015e73300000b8400bff796c20fa8b25ff7a42686338e06417a2966e85a0fc2723c928bef6cd19d34cf5e7d55ada33080613012dadb79e0278e59d9e7ed7d2d6102912a5f768c2e75b60099225c3d8bfe0c123240188b80dbee89b9b3bd5b13ccc662abc37e2129b6968adac9aba43aa778c0fe4fe337591ee87a96a29a013debc83555293c877144fc676aa1b03782c501949521a320adf6ad96c4f2e036b52a18369c637fdc49033696a84d03a69580b953187fce5aca6fb26fc8815da9f3b513bfe8e304f33ecb4b521aeb7d09c4a284ea66123bd0d6a358b2526d762ca110e1f7f20b3038d774b64d5cfd34e2213765828359d7afc5bf24d5270e99d80c3c1568fa01624b6ea1e9ce4e6890ce9bacf6611a45d41e2671f68f5b096446bf08d27ce75608425b2e3ab92146229ad1fcd8224aca5b5f73960506e7df07bfbf3664348e8ecbfb2eb467b9cfe412cb377a6ee2eb5fd11be9cf9208fe9a74c296f52cfa02a1eb0519ad9a8349bf6ccd6495feb7e391451bf96e08a0798883dee5974e47cbf3b51f111b6d3"; - String SIG2f = "c2c13b0400010a006f058262c806350910fbfcc82a015e7330471400000000001e002073616c74406e6f746174696f6e732e736571756f69612d7067702e6f7267a4d9c117dc7ba3a7e9270856f128d2ab271743eac3cb5750b22a89bd5fd60753162104d1a66e1a23b182c9980f788cfbfcc82a015e73300000b8400bff796c20fa8b25ff7a42686338e06417a2966e85a0fc2723c928bef6cd19d34cf5e7d55ada33080613012dadb79e0278e59d9e7ed7d2d6102912a5f768c2e75b60099225c3d8bfe0c123240188b80dbee89b9b3bd5b13ccc662abc37e2129b6968adac9aba43aa778c0fe4fe337591ee87a96a29a013debc83555293c877144fc676aa1b03782c501949521a320adf6ad96c4f2e036b52a18369c637fdc49033696a84d03a69580b953187fce5aca6fb26fc8815da9f3b513bfe8e304f33ecb4b521aeb7d09c4a284ea66123bd0d6a358b2526d762ca110e1f7f20b3038d774b64d5cfd34e2213765828359d7afc5bf24d5270e99d80c3c1568fa01624b6ea1e9ce4e6890ce9bacf6611a45d41e2671f68f5b096446bf08d27ce75608425b2e3ab92146229ad1fcd8224aca5b5f73960506e7df07bfbf3664348e8ecbfb2eb467b9cfe412cb377a6ee2eb5fd11be9cf9208fe9a74c296f52cfa02a1eb0519ad9a8349bf6ccd6495feb7e391451bf96e08a0798883dee5974e47cbf3b51f111b6d3"; - String out = ""; - - String hex = Hex.toHexString(bytes.toByteArray()); - while (hex.contains(OPS)) { - hex = hex.replace(OPS, "[OPS]"); - } - while (hex.contains(LIT_H)) { - hex = hex.replace(LIT_H, "[LIT]"); - } - while (hex.contains(LIT)) { - hex = hex.replace(LIT, ""); - } - while (hex.contains(SIG1)) { - hex = hex.replace(SIG1, "[SIG1]"); - } - while (hex.contains(SIG1f)) { - hex = hex.replace(SIG1f, "[SIG1f]"); - } - while (hex.contains(SIG2)) { - hex = hex.replace(SIG2, "[SIG2]"); - } - while (hex.contains(SIG2f)) { - hex = hex.replace(SIG2f, "[SIG2f]"); - } - - return out + hex; - } } - static class OPS { + static class OnePassSignature { ByteArrayOutputStream bytes = new ByteArrayOutputStream(); PGPOnePassSignature opSignature; PGPSignature signature; @@ -825,7 +803,7 @@ public class OpenPgpMessageInputStream extends DecryptionStream { boolean finished; boolean valid; - public OPS(PGPOnePassSignature signature, PGPPublicKeyRing certificate, long keyId) { + public OnePassSignature(PGPOnePassSignature signature, PGPPublicKeyRing certificate, long keyId) { this.opSignature = signature; this.certificate = certificate; this.keyId = keyId; @@ -836,6 +814,10 @@ public class OpenPgpMessageInputStream extends DecryptionStream { } public boolean verify(PGPSignature signature) { + if (finished) { + throw new IllegalStateException("Already finished."); + } + if (this.opSignature.getKeyID() != signature.getKeyID()) { // nope return false; @@ -852,8 +834,7 @@ public class OpenPgpMessageInputStream extends DecryptionStream { public void update(byte b) { if (finished) { - log("Updating finished sig!"); - return; + throw new IllegalStateException("Already finished."); } opSignature.update(b); bytes.write(b); @@ -861,49 +842,11 @@ public class OpenPgpMessageInputStream extends DecryptionStream { public void update(byte[] bytes, int off, int len) { if (finished) { - log("Updating finished sig!"); - return; + throw new IllegalStateException("Already finished."); } opSignature.update(bytes, off, len); this.bytes.write(bytes, off, len); } - - @Override - public String toString() { - String OPS = "c40d03000a01fbfcc82a015e733001"; - String LIT_H = "cb28620000000000"; - String LIT = "656e637279707420e28898207369676e20e28898207369676e20e28898207369676e"; - String SIG1 = "c2c10400010a006f058262c806350910fbfcc82a015e7330471400000000001e002073616c74406e6f746174696f6e732e736571756f69612d7067702e6f7267b0409ed8ea96dac66447bdff5b7b60c9f80a0ab91d257029153dc3b6d8c27b98162104d1a66e1a23b182c9980f788cfbfcc82a015e7330000029640c00846b5096d92474fd446cc7edaf9f14572cab93a80e12384c1e829f95debc6e8373c2ce5402be53dc1a18cf92a0ed909e0fb38855713ef8ffb13502ffac7c830fa254cc1aa6c666a97b0cc3bc176538f6913d3b8e8981a65cc42df10e0f39e4d0a06dfe961437b59a71892f4fca1116aed15123ea0d86a7b2ce47dd9d3ef22d920631bc011e82babe03ad5d72b3ba7f95bf646f20ccf6f7a4d95de37397c76c7d53741458e51ab6074007f61181c7b88b7c98f5b7510c8dfa3be01f4841501679478b15c5249d928e2a10d15ec63efa1500b994d5bfb32ffb174a976116930eb97a111e6dfd4c5e43e04a5d76ba74806a62fda63a8c3f53f6eebaf852892340e81dd08bbf348454a2cf525aeb512cf33aeeee78465ee4c442e41cc45ac4e3bb0c3333677aa60332ee7f464d9020f8554b82d619872477cca18d8431888f4ae8abe5894e9720f759c410cd7991db12703dc147040dd0d3758223e0b75de6ceae49c1a0c2c45efedeb7114ae785cc886afdc45c82172e4476e1ab5b86dc4314dd76"; - String SIG1f = "c2c13b0400010a006f058262c806350910fbfcc82a015e7330471400000000001e002073616c74406e6f746174696f6e732e736571756f69612d7067702e6f7267b0409ed8ea96dac66447bdff5b7b60c9f80a0ab91d257029153dc3b6d8c27b98162104d1a66e1a23b182c9980f788cfbfcc82a015e7330000029640c00846b5096d92474fd446cc7edaf9f14572cab93a80e12384c1e829f95debc6e8373c2ce5402be53dc1a18cf92a0ed909e0fb38855713ef8ffb13502ffac7c830fa254cc1aa6c666a97b0cc3bc176538f6913d3b8e8981a65cc42df10e0f39e4d0a06dfe961437b59a71892f4fca1116aed15123ea0d86a7b2ce47dd9d3ef22d920631bc011e82babe03ad5d72b3ba7f95bf646f20ccf6f7a4d95de37397c76c7d53741458e51ab6074007f61181c7b88b7c98f5b7510c8dfa3be01f4841501679478b15c5249d928e2a10d15ec63efa1500b994d5bfb32ffb174a976116930eb97a111e6dfd4c5e43e04a5d76ba74806a62fda63a8c3f53f6eebaf852892340e81dd08bbf348454a2cf525aeb512cf33aeeee78465ee4c442e41cc45ac4e3bb0c3333677aa60332ee7f464d9020f8554b82d619872477cca18d8431888f4ae8abe5894e9720f759c410cd7991db12703dc147040dd0d3758223e0b75de6ceae49c1a0c2c45efedeb7114ae785cc886afdc45c82172e4476e1ab5b86dc4314dd76"; - String SIG2 = "c2c10400010a006f058262c806350910fbfcc82a015e7330471400000000001e002073616c74406e6f746174696f6e732e736571756f69612d7067702e6f7267a4d9c117dc7ba3a7e9270856f128d2ab271743eac3cb5750b22a89bd5fd60753162104d1a66e1a23b182c9980f788cfbfcc82a015e73300000b8400bff796c20fa8b25ff7a42686338e06417a2966e85a0fc2723c928bef6cd19d34cf5e7d55ada33080613012dadb79e0278e59d9e7ed7d2d6102912a5f768c2e75b60099225c3d8bfe0c123240188b80dbee89b9b3bd5b13ccc662abc37e2129b6968adac9aba43aa778c0fe4fe337591ee87a96a29a013debc83555293c877144fc676aa1b03782c501949521a320adf6ad96c4f2e036b52a18369c637fdc49033696a84d03a69580b953187fce5aca6fb26fc8815da9f3b513bfe8e304f33ecb4b521aeb7d09c4a284ea66123bd0d6a358b2526d762ca110e1f7f20b3038d774b64d5cfd34e2213765828359d7afc5bf24d5270e99d80c3c1568fa01624b6ea1e9ce4e6890ce9bacf6611a45d41e2671f68f5b096446bf08d27ce75608425b2e3ab92146229ad1fcd8224aca5b5f73960506e7df07bfbf3664348e8ecbfb2eb467b9cfe412cb377a6ee2eb5fd11be9cf9208fe9a74c296f52cfa02a1eb0519ad9a8349bf6ccd6495feb7e391451bf96e08a0798883dee5974e47cbf3b51f111b6d3"; - String SIG2f = "c2c13b0400010a006f058262c806350910fbfcc82a015e7330471400000000001e002073616c74406e6f746174696f6e732e736571756f69612d7067702e6f7267a4d9c117dc7ba3a7e9270856f128d2ab271743eac3cb5750b22a89bd5fd60753162104d1a66e1a23b182c9980f788cfbfcc82a015e73300000b8400bff796c20fa8b25ff7a42686338e06417a2966e85a0fc2723c928bef6cd19d34cf5e7d55ada33080613012dadb79e0278e59d9e7ed7d2d6102912a5f768c2e75b60099225c3d8bfe0c123240188b80dbee89b9b3bd5b13ccc662abc37e2129b6968adac9aba43aa778c0fe4fe337591ee87a96a29a013debc83555293c877144fc676aa1b03782c501949521a320adf6ad96c4f2e036b52a18369c637fdc49033696a84d03a69580b953187fce5aca6fb26fc8815da9f3b513bfe8e304f33ecb4b521aeb7d09c4a284ea66123bd0d6a358b2526d762ca110e1f7f20b3038d774b64d5cfd34e2213765828359d7afc5bf24d5270e99d80c3c1568fa01624b6ea1e9ce4e6890ce9bacf6611a45d41e2671f68f5b096446bf08d27ce75608425b2e3ab92146229ad1fcd8224aca5b5f73960506e7df07bfbf3664348e8ecbfb2eb467b9cfe412cb377a6ee2eb5fd11be9cf9208fe9a74c296f52cfa02a1eb0519ad9a8349bf6ccd6495feb7e391451bf96e08a0798883dee5974e47cbf3b51f111b6d3"; - String out = "last=" + opSignature.isContaining() + "\n"; - - String hex = Hex.toHexString(bytes.toByteArray()); - while (hex.contains(OPS)) { - hex = hex.replace(OPS, "[OPS]"); - } - while (hex.contains(LIT_H)) { - hex = hex.replace(LIT_H, "[LIT]"); - } - while (hex.contains(LIT)) { - hex = hex.replace(LIT, ""); - } - while (hex.contains(SIG1)) { - hex = hex.replace(SIG1, "[SIG1]"); - } - while (hex.contains(SIG1f)) { - hex = hex.replace(SIG1f, "[SIG1f]"); - } - while (hex.contains(SIG2)) { - hex = hex.replace(SIG2, "[SIG2]"); - } - while (hex.contains(SIG2f)) { - hex = hex.replace(SIG2f, "[SIG2f]"); - } - - return out + hex; - } } } @@ -916,7 +859,7 @@ public class OpenPgpMessageInputStream extends DecryptionStream { resultBuilder.setFileEncoding(m.getFormat()); resultBuilder.setSessionKey(m.getSessionKey()); - for (Signatures.OPS ops : signatures.onePassSignatures) { + for (Signatures.OnePassSignature ops : signatures.onePassSignatures) { if (!ops.finished) { continue; } @@ -930,7 +873,7 @@ public class OpenPgpMessageInputStream extends DecryptionStream { } } - for (Signatures.SIG prep : signatures.prependedSignatures) { + for (Signatures.DetachedOrPrependedSignature prep : signatures.prependedSignatures) { if (!prep.finished) { continue; } @@ -944,7 +887,7 @@ public class OpenPgpMessageInputStream extends DecryptionStream { } } - for (Signatures.SIG det : signatures.detachedSignatures) { + for (Signatures.DetachedOrPrependedSignature det : signatures.detachedSignatures) { if (!det.finished) { continue; } @@ -964,7 +907,7 @@ public class OpenPgpMessageInputStream extends DecryptionStream { static void log(String message) { LOGGER.debug(message); // CHECKSTYLE:OFF - // System.out.println(message); + System.out.println(message); // CHECKSTYLE:ON } 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 1955eebe..87ae27f8 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 @@ -4,6 +4,7 @@ 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 static org.junit.jupiter.api.Assertions.assertTrue; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; @@ -331,6 +332,8 @@ public class OpenPgpMessageInputStreamTest { assertEquals("", metadata.getFilename()); JUtils.assertDateEquals(new Date(0L), metadata.getModificationDate()); assertEquals(StreamEncoding.BINARY, metadata.getFormat()); + assertTrue(metadata.getVerifiedSignatures().isEmpty()); + assertTrue(metadata.getRejectedSignatures().isEmpty()); } @ParameterizedTest(name = "Process LIT LIT using {0}") @@ -349,6 +352,8 @@ public class OpenPgpMessageInputStreamTest { assertEquals(PLAINTEXT, plain); MessageMetadata metadata = result.getB(); assertEquals(CompressionAlgorithm.ZIP, metadata.getCompressionAlgorithm()); + assertTrue(metadata.getVerifiedSignatures().isEmpty()); + assertTrue(metadata.getRejectedSignatures().isEmpty()); } @ParameterizedTest(name = "Process COMP using {0}") @@ -372,6 +377,8 @@ public class OpenPgpMessageInputStreamTest { assertEquals(CompressionAlgorithm.BZIP2, compressionAlgorithms.next()); assertFalse(compressionAlgorithms.hasNext()); assertNull(metadata.getEncryptionAlgorithm()); + assertTrue(metadata.getVerifiedSignatures().isEmpty()); + assertTrue(metadata.getRejectedSignatures().isEmpty()); } @ParameterizedTest(name = "Process SIG COMP(LIT) using {0}") @@ -388,6 +395,8 @@ public class OpenPgpMessageInputStreamTest { MessageMetadata metadata = result.getB(); assertEquals(CompressionAlgorithm.ZIP, metadata.getCompressionAlgorithm()); assertNull(metadata.getEncryptionAlgorithm()); + assertFalse(metadata.getVerifiedSignatures().isEmpty()); + assertTrue(metadata.getRejectedSignatures().isEmpty()); } @ParameterizedTest(name = "Process SENC(LIT) using {0}") @@ -401,6 +410,8 @@ public class OpenPgpMessageInputStreamTest { MessageMetadata metadata = result.getB(); assertNull(metadata.getCompressionAlgorithm()); assertEquals(SymmetricKeyAlgorithm.AES_256, metadata.getEncryptionAlgorithm()); + assertTrue(metadata.getVerifiedSignatures().isEmpty()); + assertTrue(metadata.getRejectedSignatures().isEmpty()); } @ParameterizedTest(name = "Process PENC(COMP(LIT)) using {0}") @@ -415,6 +426,8 @@ public class OpenPgpMessageInputStreamTest { MessageMetadata metadata = result.getB(); assertEquals(CompressionAlgorithm.ZLIB, metadata.getCompressionAlgorithm()); assertEquals(SymmetricKeyAlgorithm.AES_256, metadata.getEncryptionAlgorithm()); + assertTrue(metadata.getVerifiedSignatures().isEmpty()); + assertTrue(metadata.getRejectedSignatures().isEmpty()); } @ParameterizedTest(name = "Process OPS LIT SIG using {0}") @@ -429,6 +442,8 @@ public class OpenPgpMessageInputStreamTest { MessageMetadata metadata = result.getB(); assertNull(metadata.getEncryptionAlgorithm()); assertNull(metadata.getCompressionAlgorithm()); + assertFalse(metadata.getVerifiedSignatures().isEmpty()); + assertTrue(metadata.getRejectedSignatures().isEmpty()); } String BOB_KEY = "-----BEGIN PGP PRIVATE KEY BLOCK-----\n" + @@ -564,6 +579,8 @@ public class OpenPgpMessageInputStreamTest { MessageMetadata metadata = result.getB(); assertEquals(SymmetricKeyAlgorithm.AES_256, metadata.getEncryptionAlgorithm()); assertNull(metadata.getCompressionAlgorithm()); + assertFalse(metadata.getVerifiedSignatures().isEmpty()); + assertTrue(metadata.getRejectedSignatures().isEmpty()); } @ParameterizedTest(name = "Process PENC(OPS OPS OPS LIT SIG SIG SIG) using {0}") @@ -627,6 +644,8 @@ public class OpenPgpMessageInputStreamTest { MessageMetadata metadata = result.getB(); assertEquals(SymmetricKeyAlgorithm.AES_256, metadata.getEncryptionAlgorithm()); assertNull(metadata.getCompressionAlgorithm()); + assertFalse(metadata.getVerifiedSignatures().isEmpty()); + assertTrue(metadata.getRejectedSignatures().isEmpty()); } private static Tuple processReadBuffered(String armoredMessage, ConsumerOptions options) From da6582e1d371085291912d713d3939b559455c39 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Sun, 16 Oct 2022 18:54:22 +0200 Subject: [PATCH 28/80] Properly expose signatures --- .../MessageMetadata.java | 87 ++++++++-- .../OpenPgpMessageInputStream.java | 162 +++++++++--------- .../OpenPgpMessageInputStreamTest.java | 36 ++-- 3 files changed, 171 insertions(+), 114 deletions(-) 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 index bb2f5c76..2cd2a6f2 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/MessageMetadata.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/MessageMetadata.java @@ -91,16 +91,24 @@ public class MessageMetadata { }; } - public @Nonnull List getVerifiedSignatures() { + public List getVerifiedDetachedSignatures() { + return new ArrayList<>(message.verifiedDetachedSignatures); + } + + public List getRejectedDetachedSignatures() { + return new ArrayList<>(message.rejectedDetachedSignatures); + } + + public @Nonnull List getVerifiedInlineSignatures() { List verifications = new ArrayList<>(); - Iterator> verificationsByLayer = getVerifiedSignaturesByLayer(); + Iterator> verificationsByLayer = getVerifiedInlineSignaturesByLayer(); while (verificationsByLayer.hasNext()) { verifications.addAll(verificationsByLayer.next()); } return verifications; } - public @Nonnull Iterator> getVerifiedSignaturesByLayer() { + public @Nonnull Iterator> getVerifiedInlineSignaturesByLayer() { return new LayerIterator>(message) { @Override boolean matches(Nested layer) { @@ -114,21 +122,24 @@ public class MessageMetadata { @Override List getProperty(Layer last) { - return new ArrayList<>(last.getVerifiedSignatures()); + List list = new ArrayList<>(); + list.addAll(last.getVerifiedOnePassSignatures()); + list.addAll(last.getVerifiedPrependedSignatures()); + return list; } }; } - public @Nonnull List getRejectedSignatures() { + public @Nonnull List getRejectedInlineSignatures() { List rejected = new ArrayList<>(); - Iterator> rejectedByLayer = getRejectedSignaturesByLayer(); + Iterator> rejectedByLayer = getRejectedInlineSignaturesByLayer(); while (rejectedByLayer.hasNext()) { rejected.addAll(rejectedByLayer.next()); } return rejected; } - public @Nonnull Iterator> getRejectedSignaturesByLayer() { + public @Nonnull Iterator> getRejectedInlineSignaturesByLayer() { return new LayerIterator>(message) { @Override boolean matches(Nested layer) { @@ -142,7 +153,10 @@ public class MessageMetadata { @Override List getProperty(Layer last) { - return new ArrayList<>(last.getFailedSignatures()); + List list = new ArrayList<>(); + list.addAll(last.getRejectedOnePassSignatures()); + list.addAll(last.getRejectedPrependedSignatures()); + return list; } }; } @@ -169,8 +183,12 @@ public class MessageMetadata { } public abstract static class Layer { - protected final List verifiedSignatures = new ArrayList<>(); - protected final List failedSignatures = new ArrayList<>(); + protected final List verifiedDetachedSignatures = new ArrayList<>(); + protected final List rejectedDetachedSignatures = new ArrayList<>(); + protected final List verifiedOnePassSignatures = new ArrayList<>(); + protected final List rejectedOnePassSignatures = new ArrayList<>(); + protected final List verifiedPrependedSignatures = new ArrayList<>(); + protected final List rejectedPrependedSignatures = new ArrayList<>(); protected Nested child; public Nested getChild() { @@ -181,21 +199,54 @@ public class MessageMetadata { this.child = child; } - public List getVerifiedSignatures() { - return new ArrayList<>(verifiedSignatures); + public List getVerifiedDetachedSignatures() { + return new ArrayList<>(verifiedDetachedSignatures); } - public List getFailedSignatures() { - return new ArrayList<>(failedSignatures); + public List getRejectedDetachedSignatures() { + return new ArrayList<>(rejectedDetachedSignatures); } - void addVerifiedSignature(SignatureVerification signatureVerification) { - verifiedSignatures.add(signatureVerification); + void addVerifiedDetachedSignature(SignatureVerification signatureVerification) { + verifiedDetachedSignatures.add(signatureVerification); } - void addFailedSignature(SignatureVerification.Failure failure) { - failedSignatures.add(failure); + void addRejectedDetachedSignature(SignatureVerification.Failure failure) { + rejectedDetachedSignatures.add(failure); } + + public List getVerifiedOnePassSignatures() { + return new ArrayList<>(verifiedOnePassSignatures); + } + + public List getRejectedOnePassSignatures() { + return new ArrayList<>(rejectedOnePassSignatures); + } + + void addVerifiedOnePassSignature(SignatureVerification verifiedOnePassSignature) { + this.verifiedOnePassSignatures.add(verifiedOnePassSignature); + } + + void addRejectedOnePassSignature(SignatureVerification.Failure rejected) { + this.rejectedOnePassSignatures.add(rejected); + } + + public List getVerifiedPrependedSignatures() { + return new ArrayList<>(verifiedPrependedSignatures); + } + + public List getRejectedPrependedSignatures() { + return new ArrayList<>(rejectedPrependedSignatures); + } + + void addVerifiedPrependedSignature(SignatureVerification verified) { + this.verifiedPrependedSignatures.add(verified); + } + + void addRejectedPrependedSignature(SignatureVerification.Failure rejected) { + this.rejectedPrependedSignatures.add(rejected); + } + } public interface Nested { 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 9e26dc20..a4b05bbe 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 @@ -29,6 +29,7 @@ import org.bouncycastle.openpgp.PGPPublicKeyEncryptedData; import org.bouncycastle.openpgp.PGPPublicKeyRing; import org.bouncycastle.openpgp.PGPSecretKey; import org.bouncycastle.openpgp.PGPSecretKeyRing; +import org.bouncycastle.openpgp.PGPSessionKey; import org.bouncycastle.openpgp.PGPSignature; import org.bouncycastle.openpgp.operator.PBEDataDecryptorFactory; import org.bouncycastle.openpgp.operator.PGPContentVerifierBuilderProvider; @@ -55,6 +56,7 @@ import org.pgpainless.key.protection.SecretKeyRingProtector; import org.pgpainless.key.protection.UnlockSecretKey; import org.pgpainless.policy.Policy; import org.pgpainless.signature.SignatureUtils; +import org.pgpainless.signature.consumer.SignatureValidator; import org.pgpainless.util.Passphrase; import org.pgpainless.util.SessionKey; import org.pgpainless.util.Tuple; @@ -94,9 +96,9 @@ public class OpenPgpMessageInputStream extends DecryptionStream { } protected OpenPgpMessageInputStream(@Nonnull InputStream inputStream, - @Nonnull ConsumerOptions options, - @Nonnull MessageMetadata.Layer metadata, - @Nonnull Policy policy) + @Nonnull ConsumerOptions options, + @Nonnull MessageMetadata.Layer metadata, + @Nonnull Policy policy) throws PGPException, IOException { super(OpenPgpMetadata.getBuilder()); @@ -173,7 +175,7 @@ public class OpenPgpMessageInputStream extends DecryptionStream { throw new MissingDecryptionMethodException("No working decryption method found."); - // Marker Packets need to be skipped and ignored + // Marker Packets need to be skipped and ignored case MARKER: packetInputStream.readMarker(); break; @@ -236,7 +238,7 @@ public class OpenPgpMessageInputStream extends DecryptionStream { PGPSignature signature = packetInputStream.readSignature(); if (isSigForOPS) { signatures.leaveNesting(); // TODO: Only leave nesting if all OPSs of the nesting layer are dealt with - signatures.addCorrespondingOnePassSignature(signature, metadata); + signatures.addCorrespondingOnePassSignature(signature, metadata, policy); } else { signatures.addPrependedSignature(signature); } @@ -257,7 +259,6 @@ public class OpenPgpMessageInputStream extends DecryptionStream { // Try session key if (options.getSessionKey() != null) { SessionKey sessionKey = options.getSessionKey(); - throwIfUnacceptable(sessionKey.getAlgorithm()); SessionKeyDataDecryptorFactory decryptorFactory = ImplementationFactory.getInstance() @@ -270,11 +271,13 @@ public class OpenPgpMessageInputStream extends DecryptionStream { if (esk instanceof PGPPBEEncryptedData) { PGPPBEEncryptedData skesk = (PGPPBEEncryptedData) esk; InputStream decrypted = skesk.getDataStream(decryptorFactory); + encryptedData.sessionKey = sessionKey; nestedInputStream = new OpenPgpMessageInputStream(buffer(decrypted), options, encryptedData, policy); return true; } else if (esk instanceof PGPPublicKeyEncryptedData) { PGPPublicKeyEncryptedData pkesk = (PGPPublicKeyEncryptedData) esk; InputStream decrypted = pkesk.getDataStream(decryptorFactory); + encryptedData.sessionKey = sessionKey; nestedInputStream = new OpenPgpMessageInputStream(buffer(decrypted), options, encryptedData, policy); return true; } else { @@ -290,22 +293,22 @@ public class OpenPgpMessageInputStream extends DecryptionStream { SymmetricKeyAlgorithm kekAlgorithm = SymmetricKeyAlgorithm.requireFromId(skesk.getAlgorithm()); throwIfUnacceptable(kekAlgorithm); for (Passphrase passphrase : options.getDecryptionPassphrases()) { - PBEDataDecryptorFactory decryptorFactory = ImplementationFactory.getInstance() - .getPBEDataDecryptorFactory(passphrase); + PBEDataDecryptorFactory decryptorFactory = ImplementationFactory.getInstance() + .getPBEDataDecryptorFactory(passphrase); - try { - InputStream decrypted = skesk.getDataStream(decryptorFactory); - SymmetricKeyAlgorithm sessionKeyAlgorithm = SymmetricKeyAlgorithm.requireFromId( - skesk.getSymmetricAlgorithm(decryptorFactory)); - throwIfUnacceptable(sessionKeyAlgorithm); - MessageMetadata.EncryptedData encryptedData = new MessageMetadata.EncryptedData(sessionKeyAlgorithm); - nestedInputStream = new OpenPgpMessageInputStream(buffer(decrypted), options, encryptedData, policy); - return true; - } catch (UnacceptableAlgorithmException e) { - throw e; - } catch (PGPException e) { - // Password mismatch? - } + try { + InputStream decrypted = skesk.getDataStream(decryptorFactory); + SessionKey sessionKey = new SessionKey(skesk.getSessionKey(decryptorFactory)); + throwIfUnacceptable(sessionKey.getAlgorithm()); + MessageMetadata.EncryptedData encryptedData = new MessageMetadata.EncryptedData(sessionKey.getAlgorithm()); + encryptedData.sessionKey = sessionKey; + nestedInputStream = new OpenPgpMessageInputStream(buffer(decrypted), options, encryptedData, policy); + return true; + } catch (UnacceptableAlgorithmException e) { + throw e; + } catch (PGPException e) { + // Password mismatch? + } } } @@ -323,11 +326,13 @@ public class OpenPgpMessageInputStream extends DecryptionStream { PublicKeyDataDecryptorFactory decryptorFactory = ImplementationFactory.getInstance() .getPublicKeyDataDecryptorFactory(privateKey); try { - SymmetricKeyAlgorithm symAlg = SymmetricKeyAlgorithm.requireFromId(pkesk.getSymmetricAlgorithm(decryptorFactory)); - throwIfUnacceptable(symAlg); InputStream decrypted = pkesk.getDataStream(decryptorFactory); + SessionKey sessionKey = new SessionKey(pkesk.getSessionKey(decryptorFactory)); + throwIfUnacceptable(sessionKey.getAlgorithm()); + MessageMetadata.EncryptedData encryptedData = new MessageMetadata.EncryptedData( SymmetricKeyAlgorithm.requireFromId(pkesk.getSymmetricAlgorithm(decryptorFactory))); + encryptedData.sessionKey = sessionKey; nestedInputStream = new OpenPgpMessageInputStream(buffer(decrypted), options, encryptedData, policy); return true; @@ -348,8 +353,12 @@ public class OpenPgpMessageInputStream extends DecryptionStream { try { InputStream decrypted = pkesk.getDataStream(decryptorFactory); + SessionKey sessionKey = new SessionKey(pkesk.getSessionKey(decryptorFactory)); + throwIfUnacceptable(sessionKey.getAlgorithm()); + MessageMetadata.EncryptedData encryptedData = new MessageMetadata.EncryptedData( SymmetricKeyAlgorithm.requireFromId(pkesk.getSymmetricAlgorithm(decryptorFactory))); + encryptedData.sessionKey = sessionKey; nestedInputStream = new OpenPgpMessageInputStream(buffer(decrypted), options, encryptedData, policy); return true; } catch (PGPException e) { @@ -427,7 +436,7 @@ public class OpenPgpMessageInputStream extends DecryptionStream { } catch (PGPException e) { throw new RuntimeException(e); } - signatures.finish(metadata); + signatures.finish(metadata, policy); } return r; } @@ -455,7 +464,7 @@ public class OpenPgpMessageInputStream extends DecryptionStream { } catch (PGPException e) { throw new RuntimeException(e); } - signatures.finish(metadata); + signatures.finish(metadata, policy); } return r; } @@ -588,7 +597,7 @@ public class OpenPgpMessageInputStream extends DecryptionStream { } } - void addCorrespondingOnePassSignature(PGPSignature signature, MessageMetadata.Layer layer) { + void addCorrespondingOnePassSignature(PGPSignature signature, MessageMetadata.Layer layer, Policy policy) { for (int i = onePassSignatures.size() - 1; i >= 0; i--) { OnePassSignature onePassSignature = onePassSignatures.get(i); if (onePassSignature.opSignature.getKeyID() != signature.getKeyID()) { @@ -598,19 +607,32 @@ public class OpenPgpMessageInputStream extends DecryptionStream { continue; } - boolean verified = onePassSignature.verify(signature); + boolean correct = onePassSignature.verify(signature); SignatureVerification verification = new SignatureVerification(signature, new SubkeyIdentifier(onePassSignature.certificate, onePassSignature.keyId)); - if (verified) { - layer.addVerifiedSignature(verification); + if (correct) { + PGPPublicKey signingKey = onePassSignature.certificate.getPublicKey(onePassSignature.keyId); + try { + checkSignatureValidity(signature, signingKey, policy); + layer.addVerifiedOnePassSignature(verification); + } catch (SignatureValidationException e) { + layer.addRejectedOnePassSignature(new SignatureVerification.Failure(verification, e)); + } } else { - layer.addFailedSignature(new SignatureVerification.Failure(verification, - new SignatureValidationException("Incorrect Signature."))); + layer.addRejectedOnePassSignature(new SignatureVerification.Failure(verification, + new SignatureValidationException("Bad Signature."))); } break; } } + boolean checkSignatureValidity(PGPSignature signature, PGPPublicKey signingKey, Policy policy) throws SignatureValidationException { + SignatureValidator.wasPossiblyMadeByKey(signingKey).verify(signature); + SignatureValidator.signatureStructureIsAcceptable(signingKey, policy).verify(signature); + SignatureValidator.signatureIsEffective().verify(signature); + return true; + } + void enterNesting() { opsUpdateStack.push(literalOPS); literalOPS = new ArrayList<>(); @@ -699,27 +721,39 @@ public class OpenPgpMessageInputStream extends DecryptionStream { } } - public void finish(MessageMetadata.Layer layer) { + public void finish(MessageMetadata.Layer layer, Policy policy) { for (DetachedOrPrependedSignature detached : detachedSignatures) { - boolean verified = detached.verify(); + boolean correct = detached.verify(); SignatureVerification verification = new SignatureVerification( detached.signature, new SubkeyIdentifier(detached.certificate, detached.keyId)); - if (verified) { - layer.addVerifiedSignature(verification); + if (correct) { + try { + PGPPublicKey signingKey = detached.certificate.getPublicKey(detached.keyId); + checkSignatureValidity(detached.signature, signingKey, policy); + layer.addVerifiedDetachedSignature(verification); + } catch (SignatureValidationException e) { + layer.addRejectedDetachedSignature(new SignatureVerification.Failure(verification, e)); + } } else { - layer.addFailedSignature(new SignatureVerification.Failure( - verification, new SignatureValidationException("Incorrect Signature."))); + layer.addRejectedDetachedSignature(new SignatureVerification.Failure( + verification, new SignatureValidationException("Incorrect Signature."))); } } for (DetachedOrPrependedSignature prepended : prependedSignatures) { - boolean verified = prepended.verify(); + boolean correct = prepended.verify(); SignatureVerification verification = new SignatureVerification( prepended.signature, new SubkeyIdentifier(prepended.certificate, prepended.keyId)); - if (verified) { - layer.addVerifiedSignature(verification); + if (correct) { + try { + PGPPublicKey signingKey = prepended.certificate.getPublicKey(prepended.keyId); + checkSignatureValidity(prepended.signature, signingKey, policy); + layer.addVerifiedPrependedSignature(verification); + } catch (SignatureValidationException e) { + layer.addRejectedPrependedSignature(new SignatureVerification.Failure(verification, e)); + } } else { - layer.addFailedSignature(new SignatureVerification.Failure( + layer.addRejectedPrependedSignature(new SignatureVerification.Failure( verification, new SignatureValidationException("Incorrect Signature."))); } } @@ -859,46 +893,18 @@ public class OpenPgpMessageInputStream extends DecryptionStream { resultBuilder.setFileEncoding(m.getFormat()); resultBuilder.setSessionKey(m.getSessionKey()); - for (Signatures.OnePassSignature ops : signatures.onePassSignatures) { - if (!ops.finished) { - continue; - } - - SubkeyIdentifier identifier = new SubkeyIdentifier(ops.certificate, ops.keyId); - SignatureVerification verification = new SignatureVerification(ops.signature, identifier); - if (ops.valid) { - resultBuilder.addVerifiedInbandSignature(verification); - } else { - resultBuilder.addInvalidInbandSignature(verification, new SignatureValidationException("Incorrect signature.")); - } + for (SignatureVerification accepted : m.getVerifiedDetachedSignatures()) { + resultBuilder.addVerifiedDetachedSignature(accepted); + } + for (SignatureVerification.Failure rejected : m.getRejectedDetachedSignatures()) { + resultBuilder.addInvalidDetachedSignature(rejected.getSignatureVerification(), rejected.getValidationException()); } - for (Signatures.DetachedOrPrependedSignature prep : signatures.prependedSignatures) { - if (!prep.finished) { - continue; - } - - SubkeyIdentifier identifier = new SubkeyIdentifier(prep.certificate, prep.keyId); - SignatureVerification verification = new SignatureVerification(prep.signature, identifier); - if (prep.valid) { - resultBuilder.addVerifiedInbandSignature(verification); - } else { - resultBuilder.addInvalidInbandSignature(verification, new SignatureValidationException("Incorrect signature.")); - } + for (SignatureVerification accepted : m.getVerifiedInlineSignatures()) { + resultBuilder.addVerifiedInbandSignature(accepted); } - - for (Signatures.DetachedOrPrependedSignature det : signatures.detachedSignatures) { - if (!det.finished) { - continue; - } - - SubkeyIdentifier identifier = new SubkeyIdentifier(det.certificate, det.keyId); - SignatureVerification verification = new SignatureVerification(det.signature, identifier); - if (det.valid) { - resultBuilder.addVerifiedDetachedSignature(verification); - } else { - resultBuilder.addInvalidDetachedSignature(verification, new SignatureValidationException("Incorrect signature.")); - } + for (SignatureVerification.Failure rejected : m.getRejectedInlineSignatures()) { + resultBuilder.addInvalidInbandSignature(rejected.getSignatureVerification(), rejected.getValidationException()); } return resultBuilder.build(); 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 87ae27f8..a8969047 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 @@ -332,8 +332,8 @@ public class OpenPgpMessageInputStreamTest { assertEquals("", metadata.getFilename()); JUtils.assertDateEquals(new Date(0L), metadata.getModificationDate()); assertEquals(StreamEncoding.BINARY, metadata.getFormat()); - assertTrue(metadata.getVerifiedSignatures().isEmpty()); - assertTrue(metadata.getRejectedSignatures().isEmpty()); + assertTrue(metadata.getVerifiedInlineSignatures().isEmpty()); + assertTrue(metadata.getRejectedInlineSignatures().isEmpty()); } @ParameterizedTest(name = "Process LIT LIT using {0}") @@ -352,8 +352,8 @@ public class OpenPgpMessageInputStreamTest { assertEquals(PLAINTEXT, plain); MessageMetadata metadata = result.getB(); assertEquals(CompressionAlgorithm.ZIP, metadata.getCompressionAlgorithm()); - assertTrue(metadata.getVerifiedSignatures().isEmpty()); - assertTrue(metadata.getRejectedSignatures().isEmpty()); + assertTrue(metadata.getVerifiedInlineSignatures().isEmpty()); + assertTrue(metadata.getRejectedInlineSignatures().isEmpty()); } @ParameterizedTest(name = "Process COMP using {0}") @@ -377,8 +377,8 @@ public class OpenPgpMessageInputStreamTest { assertEquals(CompressionAlgorithm.BZIP2, compressionAlgorithms.next()); assertFalse(compressionAlgorithms.hasNext()); assertNull(metadata.getEncryptionAlgorithm()); - assertTrue(metadata.getVerifiedSignatures().isEmpty()); - assertTrue(metadata.getRejectedSignatures().isEmpty()); + assertTrue(metadata.getVerifiedInlineSignatures().isEmpty()); + assertTrue(metadata.getRejectedInlineSignatures().isEmpty()); } @ParameterizedTest(name = "Process SIG COMP(LIT) using {0}") @@ -395,8 +395,8 @@ public class OpenPgpMessageInputStreamTest { MessageMetadata metadata = result.getB(); assertEquals(CompressionAlgorithm.ZIP, metadata.getCompressionAlgorithm()); assertNull(metadata.getEncryptionAlgorithm()); - assertFalse(metadata.getVerifiedSignatures().isEmpty()); - assertTrue(metadata.getRejectedSignatures().isEmpty()); + assertFalse(metadata.getVerifiedInlineSignatures().isEmpty()); + assertTrue(metadata.getRejectedInlineSignatures().isEmpty()); } @ParameterizedTest(name = "Process SENC(LIT) using {0}") @@ -410,8 +410,8 @@ public class OpenPgpMessageInputStreamTest { MessageMetadata metadata = result.getB(); assertNull(metadata.getCompressionAlgorithm()); assertEquals(SymmetricKeyAlgorithm.AES_256, metadata.getEncryptionAlgorithm()); - assertTrue(metadata.getVerifiedSignatures().isEmpty()); - assertTrue(metadata.getRejectedSignatures().isEmpty()); + assertTrue(metadata.getVerifiedInlineSignatures().isEmpty()); + assertTrue(metadata.getRejectedInlineSignatures().isEmpty()); } @ParameterizedTest(name = "Process PENC(COMP(LIT)) using {0}") @@ -426,8 +426,8 @@ public class OpenPgpMessageInputStreamTest { MessageMetadata metadata = result.getB(); assertEquals(CompressionAlgorithm.ZLIB, metadata.getCompressionAlgorithm()); assertEquals(SymmetricKeyAlgorithm.AES_256, metadata.getEncryptionAlgorithm()); - assertTrue(metadata.getVerifiedSignatures().isEmpty()); - assertTrue(metadata.getRejectedSignatures().isEmpty()); + assertTrue(metadata.getVerifiedInlineSignatures().isEmpty()); + assertTrue(metadata.getRejectedInlineSignatures().isEmpty()); } @ParameterizedTest(name = "Process OPS LIT SIG using {0}") @@ -442,8 +442,8 @@ public class OpenPgpMessageInputStreamTest { MessageMetadata metadata = result.getB(); assertNull(metadata.getEncryptionAlgorithm()); assertNull(metadata.getCompressionAlgorithm()); - assertFalse(metadata.getVerifiedSignatures().isEmpty()); - assertTrue(metadata.getRejectedSignatures().isEmpty()); + assertFalse(metadata.getVerifiedInlineSignatures().isEmpty()); + assertTrue(metadata.getRejectedInlineSignatures().isEmpty()); } String BOB_KEY = "-----BEGIN PGP PRIVATE KEY BLOCK-----\n" + @@ -579,8 +579,8 @@ public class OpenPgpMessageInputStreamTest { MessageMetadata metadata = result.getB(); assertEquals(SymmetricKeyAlgorithm.AES_256, metadata.getEncryptionAlgorithm()); assertNull(metadata.getCompressionAlgorithm()); - assertFalse(metadata.getVerifiedSignatures().isEmpty()); - assertTrue(metadata.getRejectedSignatures().isEmpty()); + assertFalse(metadata.getVerifiedInlineSignatures().isEmpty()); + assertTrue(metadata.getRejectedInlineSignatures().isEmpty()); } @ParameterizedTest(name = "Process PENC(OPS OPS OPS LIT SIG SIG SIG) using {0}") @@ -644,8 +644,8 @@ public class OpenPgpMessageInputStreamTest { MessageMetadata metadata = result.getB(); assertEquals(SymmetricKeyAlgorithm.AES_256, metadata.getEncryptionAlgorithm()); assertNull(metadata.getCompressionAlgorithm()); - assertFalse(metadata.getVerifiedSignatures().isEmpty()); - assertTrue(metadata.getRejectedSignatures().isEmpty()); + assertFalse(metadata.getVerifiedInlineSignatures().isEmpty()); + assertTrue(metadata.getRejectedInlineSignatures().isEmpty()); } private static Tuple processReadBuffered(String armoredMessage, ConsumerOptions options) From 8a9ebdbb3e9b131a66fbb485e16f295559c95d03 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Sun, 16 Oct 2022 19:12:17 +0200 Subject: [PATCH 29/80] Reinstate integrity-protection and fix tests Integrity Protection is now checked when reading from the stream, not only when closing. --- .../OpenPgpMessageInputStream.java | 17 +++++++++---- .../TeeBCPGInputStream.java | 20 ++++++++++++++-- .../ModificationDetectionTests.java | 24 ++++++++++++------- 3 files changed, 46 insertions(+), 15 deletions(-) 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 a4b05bbe..54172e7f 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 @@ -272,13 +272,15 @@ public class OpenPgpMessageInputStream extends DecryptionStream { PGPPBEEncryptedData skesk = (PGPPBEEncryptedData) esk; InputStream decrypted = skesk.getDataStream(decryptorFactory); encryptedData.sessionKey = sessionKey; - nestedInputStream = new OpenPgpMessageInputStream(buffer(decrypted), options, encryptedData, policy); + IntegrityProtectedInputStream integrityProtected = new IntegrityProtectedInputStream(decrypted, skesk, options); + nestedInputStream = new OpenPgpMessageInputStream(buffer(integrityProtected), options, encryptedData, policy); return true; } else if (esk instanceof PGPPublicKeyEncryptedData) { PGPPublicKeyEncryptedData pkesk = (PGPPublicKeyEncryptedData) esk; InputStream decrypted = pkesk.getDataStream(decryptorFactory); encryptedData.sessionKey = sessionKey; - nestedInputStream = new OpenPgpMessageInputStream(buffer(decrypted), options, encryptedData, policy); + IntegrityProtectedInputStream integrityProtected = new IntegrityProtectedInputStream(decrypted, pkesk, options); + nestedInputStream = new OpenPgpMessageInputStream(buffer(integrityProtected), options, encryptedData, policy); return true; } else { throw new RuntimeException("Unknown ESK class type: " + esk.getClass().getName()); @@ -302,7 +304,8 @@ public class OpenPgpMessageInputStream extends DecryptionStream { throwIfUnacceptable(sessionKey.getAlgorithm()); MessageMetadata.EncryptedData encryptedData = new MessageMetadata.EncryptedData(sessionKey.getAlgorithm()); encryptedData.sessionKey = sessionKey; - nestedInputStream = new OpenPgpMessageInputStream(buffer(decrypted), options, encryptedData, policy); + IntegrityProtectedInputStream integrityProtected = new IntegrityProtectedInputStream(decrypted, skesk, options); + nestedInputStream = new OpenPgpMessageInputStream(buffer(integrityProtected), options, encryptedData, policy); return true; } catch (UnacceptableAlgorithmException e) { throw e; @@ -334,7 +337,8 @@ public class OpenPgpMessageInputStream extends DecryptionStream { SymmetricKeyAlgorithm.requireFromId(pkesk.getSymmetricAlgorithm(decryptorFactory))); encryptedData.sessionKey = sessionKey; - nestedInputStream = new OpenPgpMessageInputStream(buffer(decrypted), options, encryptedData, policy); + IntegrityProtectedInputStream integrityProtected = new IntegrityProtectedInputStream(decrypted, pkesk, options); + nestedInputStream = new OpenPgpMessageInputStream(buffer(integrityProtected), options, encryptedData, policy); return true; } catch (UnacceptableAlgorithmException e) { throw e; @@ -359,7 +363,9 @@ public class OpenPgpMessageInputStream extends DecryptionStream { MessageMetadata.EncryptedData encryptedData = new MessageMetadata.EncryptedData( SymmetricKeyAlgorithm.requireFromId(pkesk.getSymmetricAlgorithm(decryptorFactory))); encryptedData.sessionKey = sessionKey; - nestedInputStream = new OpenPgpMessageInputStream(buffer(decrypted), options, encryptedData, policy); + + IntegrityProtectedInputStream integrityProtected = new IntegrityProtectedInputStream(decrypted, pkesk, options); + nestedInputStream = new OpenPgpMessageInputStream(buffer(integrityProtected), options, encryptedData, policy); return true; } catch (PGPException e) { // hm :/ @@ -491,6 +497,7 @@ public class OpenPgpMessageInputStream extends DecryptionStream { automaton.next(InputAlphabet.EndOfSequence); automaton.assertValid(); + packetInputStream.close(); closed = true; } diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/TeeBCPGInputStream.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/TeeBCPGInputStream.java index f80793f0..2efcfc43 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/TeeBCPGInputStream.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/TeeBCPGInputStream.java @@ -96,6 +96,12 @@ public class TeeBCPGInputStream { return markerPacket; } + + public void close() throws IOException { + this.packetInputStream.close(); + this.delayedTee.close(); + } + public static class DelayedTeeInputStreamInputStream extends InputStream { private int last = -1; @@ -112,8 +118,12 @@ public class TeeBCPGInputStream { if (last != -1) { outputStream.write(last); } - last = inputStream.read(); - return last; + try { + last = inputStream.read(); + return last; + } catch (IOException e) { + return -1; + } } /** @@ -127,5 +137,11 @@ public class TeeBCPGInputStream { } last = -1; } + + @Override + public void close() throws IOException { + inputStream.close(); + outputStream.close(); + } } } diff --git a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/ModificationDetectionTests.java b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/ModificationDetectionTests.java index 14a041ff..9ecaa38a 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/ModificationDetectionTests.java +++ b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/ModificationDetectionTests.java @@ -238,8 +238,10 @@ public class ModificationDetectionTests { ); ByteArrayOutputStream out = new ByteArrayOutputStream(); - Streams.pipeAll(decryptionStream, out); - assertThrows(ModificationDetectionException.class, decryptionStream::close); + assertThrows(ModificationDetectionException.class, () -> { + Streams.pipeAll(decryptionStream, out); + decryptionStream.close(); + }); } @TestTemplate @@ -269,8 +271,10 @@ public class ModificationDetectionTests { ); ByteArrayOutputStream out = new ByteArrayOutputStream(); - Streams.pipeAll(decryptionStream, out); - assertThrows(ModificationDetectionException.class, decryptionStream::close); + assertThrows(ModificationDetectionException.class, () -> { + Streams.pipeAll(decryptionStream, out); + decryptionStream.close(); + }); } @TestTemplate @@ -313,8 +317,10 @@ public class ModificationDetectionTests { ); ByteArrayOutputStream out = new ByteArrayOutputStream(); - Streams.pipeAll(decryptionStream, out); - assertThrows(ModificationDetectionException.class, decryptionStream::close); + assertThrows(ModificationDetectionException.class, () -> { + Streams.pipeAll(decryptionStream, out); + decryptionStream.close(); + }); } @TestTemplate @@ -344,8 +350,10 @@ public class ModificationDetectionTests { ); ByteArrayOutputStream out = new ByteArrayOutputStream(); - Streams.pipeAll(decryptionStream, out); - assertThrows(ModificationDetectionException.class, decryptionStream::close); + assertThrows(ModificationDetectionException.class, () -> { + Streams.pipeAll(decryptionStream, out); + decryptionStream.close(); + }); } @TestTemplate From 513ab0e3ede777f33cee07365a70985052563b0e Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 17 Oct 2022 00:53:50 +0200 Subject: [PATCH 30/80] Fix checkstyle issues --- .../decryption_verification/DecryptionStreamFactory.java | 1 - .../decryption_verification/OpenPgpMessageInputStream.java | 5 ++--- .../InvestigateMultiSEIPMessageHandlingTest.java | 2 -- .../PreventDecryptionUsingNonEncryptionKeyTest.java | 2 -- 4 files changed, 2 insertions(+), 8 deletions(-) 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 680f33e4..5e6b2aec 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 @@ -40,7 +40,6 @@ 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.graalvm.compiler.lir.amd64.AMD64BinaryConsumer; import org.pgpainless.PGPainless; import org.pgpainless.algorithm.CompressionAlgorithm; import org.pgpainless.algorithm.EncryptionPurpose; 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 54172e7f..37ccff37 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 @@ -29,7 +29,6 @@ import org.bouncycastle.openpgp.PGPPublicKeyEncryptedData; import org.bouncycastle.openpgp.PGPPublicKeyRing; import org.bouncycastle.openpgp.PGPSecretKey; import org.bouncycastle.openpgp.PGPSecretKeyRing; -import org.bouncycastle.openpgp.PGPSessionKey; import org.bouncycastle.openpgp.PGPSignature; import org.bouncycastle.openpgp.operator.PBEDataDecryptorFactory; import org.bouncycastle.openpgp.operator.PGPContentVerifierBuilderProvider; @@ -795,7 +794,7 @@ public class OpenPgpMessageInputStream extends DecryptionStream { boolean finished; boolean valid; - public DetachedOrPrependedSignature(PGPSignature signature, PGPPublicKeyRing certificate, long keyId) { + DetachedOrPrependedSignature(PGPSignature signature, PGPPublicKeyRing certificate, long keyId) { this.signature = signature; this.certificate = certificate; this.keyId = keyId; @@ -844,7 +843,7 @@ public class OpenPgpMessageInputStream extends DecryptionStream { boolean finished; boolean valid; - public OnePassSignature(PGPOnePassSignature signature, PGPPublicKeyRing certificate, long keyId) { + OnePassSignature(PGPOnePassSignature signature, PGPPublicKeyRing certificate, long keyId) { this.opSignature = signature; this.certificate = certificate; this.keyId = keyId; diff --git a/pgpainless-core/src/test/java/investigations/InvestigateMultiSEIPMessageHandlingTest.java b/pgpainless-core/src/test/java/investigations/InvestigateMultiSEIPMessageHandlingTest.java index d31a0e0f..a0ea747a 100644 --- a/pgpainless-core/src/test/java/investigations/InvestigateMultiSEIPMessageHandlingTest.java +++ b/pgpainless-core/src/test/java/investigations/InvestigateMultiSEIPMessageHandlingTest.java @@ -4,8 +4,6 @@ package investigations; -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 java.io.ByteArrayInputStream; diff --git a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/PreventDecryptionUsingNonEncryptionKeyTest.java b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/PreventDecryptionUsingNonEncryptionKeyTest.java index 7474301b..ba80c69d 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/PreventDecryptionUsingNonEncryptionKeyTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/PreventDecryptionUsingNonEncryptionKeyTest.java @@ -8,8 +8,6 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.EOFException; import java.io.IOException; import java.nio.charset.StandardCharsets; From 6324455cf5ace6f206b001df9c9e208b0e69bf1e Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 17 Oct 2022 02:47:11 +0200 Subject: [PATCH 31/80] Fix NPEs and expose decryption keys --- .../MessageMetadata.java | 20 +++++++++++++ .../OpenPgpMessageInputStream.java | 30 ++++++++++++------- 2 files changed, 40 insertions(+), 10 deletions(-) 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 index 2cd2a6f2..1be47112 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/MessageMetadata.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/MessageMetadata.java @@ -7,6 +7,7 @@ package org.pgpainless.decryption_verification; import org.pgpainless.algorithm.CompressionAlgorithm; import org.pgpainless.algorithm.StreamEncoding; import org.pgpainless.algorithm.SymmetricKeyAlgorithm; +import org.pgpainless.key.SubkeyIdentifier; import org.pgpainless.util.SessionKey; import javax.annotation.Nonnull; @@ -182,6 +183,24 @@ public class MessageMetadata { return (LiteralData) nested; } + public SubkeyIdentifier getDecryptionKey() { + Iterator iterator = new LayerIterator(message) { + @Override + public boolean matches(Nested layer) { + return layer instanceof EncryptedData; + } + + @Override + public SubkeyIdentifier getProperty(Layer last) { + return ((EncryptedData) last).decryptionKey; + } + }; + if (iterator.hasNext()) { + return iterator.next(); + } + return null; + } + public abstract static class Layer { protected final List verifiedDetachedSignatures = new ArrayList<>(); protected final List rejectedDetachedSignatures = new ArrayList<>(); @@ -309,6 +328,7 @@ public class MessageMetadata { public static class EncryptedData extends Layer implements Nested { protected final SymmetricKeyAlgorithm algorithm; + protected SubkeyIdentifier decryptionKey; protected SessionKey sessionKey; protected List recipients; 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 37ccff37..944ce6e4 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 @@ -334,6 +334,7 @@ public class OpenPgpMessageInputStream extends DecryptionStream { MessageMetadata.EncryptedData encryptedData = new MessageMetadata.EncryptedData( SymmetricKeyAlgorithm.requireFromId(pkesk.getSymmetricAlgorithm(decryptorFactory))); + encryptedData.decryptionKey = new SubkeyIdentifier(decryptionKeys, decryptionKey.getKeyID()); encryptedData.sessionKey = sessionKey; IntegrityProtectedInputStream integrityProtected = new IntegrityProtectedInputStream(decrypted, pkesk, options); @@ -361,6 +362,7 @@ public class OpenPgpMessageInputStream extends DecryptionStream { MessageMetadata.EncryptedData encryptedData = new MessageMetadata.EncryptedData( SymmetricKeyAlgorithm.requireFromId(pkesk.getSymmetricAlgorithm(decryptorFactory))); + encryptedData.decryptionKey = new SubkeyIdentifier(decryptionKeyCandidate.getA(), privateKey.getKeyID()); encryptedData.sessionKey = sessionKey; IntegrityProtectedInputStream integrityProtected = new IntegrityProtectedInputStream(decrypted, pkesk, options); @@ -560,8 +562,6 @@ public class OpenPgpMessageInputStream extends DecryptionStream { final List correspondingSignatures; boolean isLiteral = true; - final List verified = new ArrayList<>(); - private Signatures(ConsumerOptions options) { this.options = options; this.detachedSignatures = new ArrayList<>(); @@ -580,24 +580,33 @@ public class OpenPgpMessageInputStream extends DecryptionStream { void addDetachedSignature(PGPSignature signature) { long keyId = SignatureUtils.determineIssuerKeyId(signature); PGPPublicKeyRing certificate = findCertificate(keyId); - initialize(signature, certificate, keyId); - this.detachedSignatures.add(new DetachedOrPrependedSignature(signature, certificate, keyId)); + + if (certificate != null) { + initialize(signature, certificate, keyId); + this.detachedSignatures.add(new DetachedOrPrependedSignature(signature, certificate, keyId)); + } } void addPrependedSignature(PGPSignature signature) { long keyId = SignatureUtils.determineIssuerKeyId(signature); PGPPublicKeyRing certificate = findCertificate(keyId); - initialize(signature, certificate, keyId); - this.prependedSignatures.add(new DetachedOrPrependedSignature(signature, certificate, keyId)); + + if (certificate != null) { + initialize(signature, certificate, keyId); + this.prependedSignatures.add(new DetachedOrPrependedSignature(signature, certificate, keyId)); + } } void addOnePassSignature(PGPOnePassSignature signature) { PGPPublicKeyRing certificate = findCertificate(signature.getKeyID()); - OnePassSignature ops = new OnePassSignature(signature, certificate, signature.getKeyID()); - ops.init(certificate); - onePassSignatures.add(ops); - literalOPS.add(ops); + if (certificate != null) { + OnePassSignature ops = new OnePassSignature(signature, certificate, signature.getKeyID()); + ops.init(certificate); + onePassSignatures.add(ops); + + literalOPS.add(ops); + } if (signature.isContaining()) { enterNesting(); } @@ -898,6 +907,7 @@ public class OpenPgpMessageInputStream extends DecryptionStream { resultBuilder.setFileName(m.getFilename()); resultBuilder.setFileEncoding(m.getFormat()); resultBuilder.setSessionKey(m.getSessionKey()); + resultBuilder.setDecryptionKey(m.getDecryptionKey()); for (SignatureVerification accepted : m.getVerifiedDetachedSignatures()) { resultBuilder.addVerifiedDetachedSignature(accepted); From 0a7c76a2dd61c766ce0db4ae4c62130ba3f59961 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 18 Oct 2022 15:55:47 +0200 Subject: [PATCH 32/80] Enfore max recursion depth and fix CRC test --- .../MessageMetadata.java | 19 ++++++++- .../OpenPgpMessageInputStream.java | 39 ++++++++++++++++--- .../org/bouncycastle/AsciiArmorCRCTests.java | 20 +++++----- .../MessageMetadataTest.java | 6 +-- .../RecursionDepthTest.java | 4 +- 5 files changed, 66 insertions(+), 22 deletions(-) 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 index 1be47112..59f8052a 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/MessageMetadata.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/MessageMetadata.java @@ -7,6 +7,7 @@ package org.pgpainless.decryption_verification; import org.pgpainless.algorithm.CompressionAlgorithm; import org.pgpainless.algorithm.StreamEncoding; import org.pgpainless.algorithm.SymmetricKeyAlgorithm; +import org.pgpainless.exception.MalformedOpenPgpMessageException; import org.pgpainless.key.SubkeyIdentifier; import org.pgpainless.util.SessionKey; @@ -202,6 +203,8 @@ public class MessageMetadata { } public abstract static class Layer { + public static final int MAX_LAYER_DEPTH = 16; + protected final int depth; protected final List verifiedDetachedSignatures = new ArrayList<>(); protected final List rejectedDetachedSignatures = new ArrayList<>(); protected final List verifiedOnePassSignatures = new ArrayList<>(); @@ -210,6 +213,13 @@ public class MessageMetadata { protected final List rejectedPrependedSignatures = new ArrayList<>(); protected Nested child; + public Layer(int depth) { + this.depth = depth; + if (depth > MAX_LAYER_DEPTH) { + throw new MalformedOpenPgpMessageException("Maximum nesting depth exceeded."); + } + } + public Nested getChild() { return child; } @@ -274,6 +284,9 @@ public class MessageMetadata { public static class Message extends Layer { + public Message() { + super(0); + } } public static class LiteralData implements Nested { @@ -312,7 +325,8 @@ public class MessageMetadata { public static class CompressedData extends Layer implements Nested { protected final CompressionAlgorithm algorithm; - public CompressedData(CompressionAlgorithm zip) { + public CompressedData(CompressionAlgorithm zip, int depth) { + super(depth); this.algorithm = zip; } @@ -332,7 +346,8 @@ public class MessageMetadata { protected SessionKey sessionKey; protected List recipients; - public EncryptedData(SymmetricKeyAlgorithm algorithm) { + public EncryptedData(SymmetricKeyAlgorithm algorithm, int depth) { + super(depth); this.algorithm = algorithm; } 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 944ce6e4..f5ed556a 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 @@ -15,6 +15,7 @@ import java.util.List; import java.util.Stack; import javax.annotation.Nonnull; +import org.bouncycastle.bcpg.ArmoredInputStream; import org.bouncycastle.bcpg.BCPGInputStream; import org.bouncycastle.openpgp.PGPCompressedData; import org.bouncycastle.openpgp.PGPEncryptedData; @@ -56,6 +57,7 @@ import org.pgpainless.key.protection.UnlockSecretKey; import org.pgpainless.policy.Policy; import org.pgpainless.signature.SignatureUtils; import org.pgpainless.signature.consumer.SignatureValidator; +import org.pgpainless.util.ArmoredInputStreamFactory; import org.pgpainless.util.Passphrase; import org.pgpainless.util.SessionKey; import org.pgpainless.util.Tuple; @@ -91,7 +93,7 @@ public class OpenPgpMessageInputStream extends DecryptionStream { @Nonnull ConsumerOptions options, @Nonnull Policy policy) throws PGPException, IOException { - this(inputStream, options, new MessageMetadata.Message(), policy); + this(prepareInputStream(inputStream, options), options, new MessageMetadata.Message(), policy); } protected OpenPgpMessageInputStream(@Nonnull InputStream inputStream, @@ -118,6 +120,26 @@ public class OpenPgpMessageInputStream extends DecryptionStream { consumePackets(); } + private static InputStream prepareInputStream(InputStream inputStream, ConsumerOptions options) throws IOException { + OpenPgpInputStream openPgpIn = new OpenPgpInputStream(inputStream); + openPgpIn.reset(); + + if (openPgpIn.isBinaryOpenPgp()) { + return openPgpIn; + } + + if (openPgpIn.isAsciiArmored()) { + ArmoredInputStream armorIn = ArmoredInputStreamFactory.get(openPgpIn); + if (armorIn.isClearText()) { + return armorIn; + } else { + return armorIn; + } + } else { + return openPgpIn; + } + } + /** * Consume OpenPGP packets from the current {@link BCPGInputStream}. * Once an OpenPGP packet with nested data (Literal Data, Compressed Data, Encrypted Data) is reached, @@ -219,7 +241,8 @@ public class OpenPgpMessageInputStream extends DecryptionStream { signatures.enterNesting(); PGPCompressedData compressedData = packetInputStream.readCompressedData(); MessageMetadata.CompressedData compressionLayer = new MessageMetadata.CompressedData( - CompressionAlgorithm.fromId(compressedData.getAlgorithm())); + CompressionAlgorithm.fromId(compressedData.getAlgorithm()), + metadata.depth + 1); InputStream decompressed = compressedData.getDataStream(); nestedInputStream = new OpenPgpMessageInputStream(buffer(decompressed), options, compressionLayer, policy); } @@ -262,7 +285,8 @@ public class OpenPgpMessageInputStream extends DecryptionStream { SessionKeyDataDecryptorFactory decryptorFactory = ImplementationFactory.getInstance() .getSessionKeyDataDecryptorFactory(sessionKey); - MessageMetadata.EncryptedData encryptedData = new MessageMetadata.EncryptedData(sessionKey.getAlgorithm()); + MessageMetadata.EncryptedData encryptedData = new MessageMetadata.EncryptedData( + sessionKey.getAlgorithm(), metadata.depth + 1); try { // TODO: Use BCs new API once shipped @@ -301,7 +325,8 @@ public class OpenPgpMessageInputStream extends DecryptionStream { InputStream decrypted = skesk.getDataStream(decryptorFactory); SessionKey sessionKey = new SessionKey(skesk.getSessionKey(decryptorFactory)); throwIfUnacceptable(sessionKey.getAlgorithm()); - MessageMetadata.EncryptedData encryptedData = new MessageMetadata.EncryptedData(sessionKey.getAlgorithm()); + MessageMetadata.EncryptedData encryptedData = new MessageMetadata.EncryptedData( + sessionKey.getAlgorithm(), metadata.depth + 1); encryptedData.sessionKey = sessionKey; IntegrityProtectedInputStream integrityProtected = new IntegrityProtectedInputStream(decrypted, skesk, options); nestedInputStream = new OpenPgpMessageInputStream(buffer(integrityProtected), options, encryptedData, policy); @@ -333,7 +358,8 @@ public class OpenPgpMessageInputStream extends DecryptionStream { throwIfUnacceptable(sessionKey.getAlgorithm()); MessageMetadata.EncryptedData encryptedData = new MessageMetadata.EncryptedData( - SymmetricKeyAlgorithm.requireFromId(pkesk.getSymmetricAlgorithm(decryptorFactory))); + SymmetricKeyAlgorithm.requireFromId(pkesk.getSymmetricAlgorithm(decryptorFactory)), + metadata.depth + 1); encryptedData.decryptionKey = new SubkeyIdentifier(decryptionKeys, decryptionKey.getKeyID()); encryptedData.sessionKey = sessionKey; @@ -361,7 +387,8 @@ public class OpenPgpMessageInputStream extends DecryptionStream { throwIfUnacceptable(sessionKey.getAlgorithm()); MessageMetadata.EncryptedData encryptedData = new MessageMetadata.EncryptedData( - SymmetricKeyAlgorithm.requireFromId(pkesk.getSymmetricAlgorithm(decryptorFactory))); + SymmetricKeyAlgorithm.requireFromId(pkesk.getSymmetricAlgorithm(decryptorFactory)), + metadata.depth + 1); encryptedData.decryptionKey = new SubkeyIdentifier(decryptionKeyCandidate.getA(), privateKey.getKeyID()); encryptedData.sessionKey = sessionKey; diff --git a/pgpainless-core/src/test/java/org/bouncycastle/AsciiArmorCRCTests.java b/pgpainless-core/src/test/java/org/bouncycastle/AsciiArmorCRCTests.java index c3af0bd6..f9bd7a61 100644 --- a/pgpainless-core/src/test/java/org/bouncycastle/AsciiArmorCRCTests.java +++ b/pgpainless-core/src/test/java/org/bouncycastle/AsciiArmorCRCTests.java @@ -489,7 +489,7 @@ public class AsciiArmorCRCTests { Passphrase passphrase = Passphrase.fromPassword("flowcrypt compatibility tests"); @Test - public void testInvalidArmorCRCThrowsOnClose() throws PGPException, IOException { + public void testInvalidArmorCRCThrowsOnClose() throws IOException { String message = "-----BEGIN PGP MESSAGE-----\n" + "Version: FlowCrypt 5.0.4 Gmail Encryption flowcrypt.com\n" + "Comment: Seamlessly send, receive and search encrypted email\n" + @@ -542,14 +542,16 @@ public class AsciiArmorCRCTests { "-----END PGP MESSAGE-----\n"; PGPSecretKeyRing key = PGPainless.readKeyRing().secretKeyRing(ASCII_KEY); - DecryptionStream decryptionStream = PGPainless.decryptAndOrVerify() - .onInputStream(new ByteArrayInputStream(message.getBytes(StandardCharsets.UTF_8))) - .withOptions(new ConsumerOptions().addDecryptionKey( - key, SecretKeyRingProtector.unlockAnyKeyWith(passphrase) - )); + assertThrows(IOException.class, () -> { + DecryptionStream decryptionStream = PGPainless.decryptAndOrVerify() + .onInputStream(new ByteArrayInputStream(message.getBytes(StandardCharsets.UTF_8))) + .withOptions(new ConsumerOptions().addDecryptionKey( + key, SecretKeyRingProtector.unlockAnyKeyWith(passphrase) + )); - ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); - Streams.pipeAll(decryptionStream, outputStream); - assertThrows(IOException.class, decryptionStream::close); + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + Streams.pipeAll(decryptionStream, outputStream); + decryptionStream.close(); + }); } } 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 index 9f887eb9..d87fc6bf 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/MessageMetadataTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/MessageMetadataTest.java @@ -28,9 +28,9 @@ public class MessageMetadataTest { // 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.CompressedData compressedData = new MessageMetadata.CompressedData(CompressionAlgorithm.ZIP, message.depth + 1); + MessageMetadata.EncryptedData encryptedData = new MessageMetadata.EncryptedData(SymmetricKeyAlgorithm.AES_128, compressedData.depth + 1); + MessageMetadata.EncryptedData encryptedData1 = new MessageMetadata.EncryptedData(SymmetricKeyAlgorithm.AES_256, encryptedData.depth + 1); MessageMetadata.LiteralData literalData = new MessageMetadata.LiteralData(); message.setChild(compressedData); diff --git a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/RecursionDepthTest.java b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/RecursionDepthTest.java index b253cf7f..52c5a906 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/RecursionDepthTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/RecursionDepthTest.java @@ -11,12 +11,12 @@ import java.io.ByteArrayOutputStream; import java.io.IOException; import java.nio.charset.StandardCharsets; -import org.bouncycastle.openpgp.PGPException; import org.bouncycastle.openpgp.PGPSecretKeyRing; import org.bouncycastle.util.io.Streams; import org.junit.jupiter.api.TestTemplate; import org.junit.jupiter.api.extension.ExtendWith; import org.pgpainless.PGPainless; +import org.pgpainless.exception.MalformedOpenPgpMessageException; import org.pgpainless.util.TestAllImplementations; public class RecursionDepthTest { @@ -143,7 +143,7 @@ public class RecursionDepthTest { "-----END PGP ARMORED FILE-----\n"; - assertThrows(PGPException.class, () -> { + assertThrows(MalformedOpenPgpMessageException.class, () -> { DecryptionStream decryptionStream = PGPainless.decryptAndOrVerify() .onInputStream(new ByteArrayInputStream(msg.getBytes(StandardCharsets.UTF_8))) .withOptions(new ConsumerOptions().addDecryptionKey(secretKey)); From 5c61559647c106fe4a3d6f1e049eced34a00d552 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 20 Oct 2022 14:05:21 +0200 Subject: [PATCH 33/80] Reuse *SignatureCheck class --- .../DecryptionStreamFactory.java | 16 +- .../OpenPgpMessageInputStream.java | 391 +++++++----------- .../SignatureInputStream.java | 12 +- ...ignatureCheck.java => SignatureCheck.java} | 6 +- 4 files changed, 170 insertions(+), 255 deletions(-) rename pgpainless-core/src/main/java/org/pgpainless/signature/consumer/{DetachedSignatureCheck.java => SignatureCheck.java} (90%) 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 5e6b2aec..0739eb19 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 @@ -60,7 +60,7 @@ import org.pgpainless.key.info.KeyRingInfo; import org.pgpainless.key.protection.SecretKeyRingProtector; import org.pgpainless.key.protection.UnlockSecretKey; import org.pgpainless.signature.SignatureUtils; -import org.pgpainless.signature.consumer.DetachedSignatureCheck; +import org.pgpainless.signature.consumer.SignatureCheck; import org.pgpainless.signature.consumer.OnePassSignatureCheck; import org.pgpainless.util.ArmoredInputStreamFactory; import org.pgpainless.util.Passphrase; @@ -79,7 +79,7 @@ public final class DecryptionStreamFactory { private final ConsumerOptions options; private final OpenPgpMetadata.Builder resultBuilder = OpenPgpMetadata.getBuilder(); private final List onePassSignatureChecks = new ArrayList<>(); - private final List detachedSignatureChecks = new ArrayList<>(); + private final List signatureChecks = new ArrayList<>(); private final Map onePassSignaturesWithMissingCert = new HashMap<>(); private static final PGPContentVerifierBuilderProvider verifierBuilderProvider = @@ -88,7 +88,7 @@ public final class DecryptionStreamFactory { public static DecryptionStream create(@Nonnull InputStream inputStream, - @Nonnull ConsumerOptions options) + @Nonnull ConsumerOptions options) throws PGPException, IOException { OpenPgpInputStream openPgpInputStream = new OpenPgpInputStream(inputStream); openPgpInputStream.reset(); @@ -134,9 +134,9 @@ public final class DecryptionStreamFactory { SubkeyIdentifier signingKeyIdentifier = new SubkeyIdentifier(signingKeyRing, signingKey.getKeyID()); try { signature.init(verifierBuilderProvider, signingKey); - DetachedSignatureCheck detachedSignature = - new DetachedSignatureCheck(signature, signingKeyRing, signingKeyIdentifier); - detachedSignatureChecks.add(detachedSignature); + SignatureCheck detachedSignature = + new SignatureCheck(signature, signingKeyRing, signingKeyIdentifier); + signatureChecks.add(detachedSignature); } catch (PGPException e) { SignatureValidationException ex = new SignatureValidationException( "Cannot verify detached signature made by " + signingKeyIdentifier + ".", e); @@ -212,7 +212,7 @@ public final class DecryptionStreamFactory { private InputStream wrapInVerifySignatureStream(InputStream bufferedIn, @Nullable PGPObjectFactory objectFactory) { return new SignatureInputStream.VerifySignatures( bufferedIn, objectFactory, onePassSignatureChecks, - onePassSignaturesWithMissingCert, detachedSignatureChecks, options, + onePassSignaturesWithMissingCert, signatureChecks, options, resultBuilder); } @@ -348,7 +348,7 @@ public final class DecryptionStreamFactory { } return new SignatureInputStream.VerifySignatures(literalDataInputStream, objectFactory, - onePassSignatureChecks, onePassSignaturesWithMissingCert, detachedSignatureChecks, options, resultBuilder) { + onePassSignatureChecks, onePassSignaturesWithMissingCert, signatureChecks, options, resultBuilder) { }; } 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 f5ed556a..101a60c8 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 @@ -5,7 +5,6 @@ package org.pgpainless.decryption_verification; import java.io.BufferedInputStream; -import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; @@ -31,6 +30,7 @@ import org.bouncycastle.openpgp.PGPPublicKeyRing; import org.bouncycastle.openpgp.PGPSecretKey; import org.bouncycastle.openpgp.PGPSecretKeyRing; import org.bouncycastle.openpgp.PGPSignature; +import org.bouncycastle.openpgp.PGPSignatureList; import org.bouncycastle.openpgp.operator.PBEDataDecryptorFactory; import org.bouncycastle.openpgp.operator.PGPContentVerifierBuilderProvider; import org.bouncycastle.openpgp.operator.PublicKeyDataDecryptorFactory; @@ -44,6 +44,8 @@ import org.pgpainless.algorithm.SymmetricKeyAlgorithm; import org.pgpainless.decryption_verification.automaton.InputAlphabet; import org.pgpainless.decryption_verification.automaton.PDA; import org.pgpainless.decryption_verification.automaton.StackAlphabet; +import org.pgpainless.decryption_verification.cleartext_signatures.ClearsignedMessageUtil; +import org.pgpainless.decryption_verification.cleartext_signatures.MultiPassStrategy; import org.pgpainless.exception.MalformedOpenPgpMessageException; import org.pgpainless.exception.MessageNotIntegrityProtectedException; import org.pgpainless.exception.MissingDecryptionMethodException; @@ -56,7 +58,9 @@ import org.pgpainless.key.protection.SecretKeyRingProtector; import org.pgpainless.key.protection.UnlockSecretKey; import org.pgpainless.policy.Policy; import org.pgpainless.signature.SignatureUtils; -import org.pgpainless.signature.consumer.SignatureValidator; +import org.pgpainless.signature.consumer.OnePassSignatureCheck; +import org.pgpainless.signature.consumer.SignatureCheck; +import org.pgpainless.signature.consumer.SignatureVerifier; import org.pgpainless.util.ArmoredInputStreamFactory; import org.pgpainless.util.Passphrase; import org.pgpainless.util.SessionKey; @@ -93,7 +97,9 @@ public class OpenPgpMessageInputStream extends DecryptionStream { @Nonnull ConsumerOptions options, @Nonnull Policy policy) throws PGPException, IOException { - this(prepareInputStream(inputStream, options), options, new MessageMetadata.Message(), policy); + this( + prepareInputStream(inputStream, options, policy), + options, new MessageMetadata.Message(), policy); } protected OpenPgpMessageInputStream(@Nonnull InputStream inputStream, @@ -120,7 +126,20 @@ public class OpenPgpMessageInputStream extends DecryptionStream { consumePackets(); } - private static InputStream prepareInputStream(InputStream inputStream, ConsumerOptions options) throws IOException { + protected OpenPgpMessageInputStream(@Nonnull InputStream inputStream, + @Nonnull Policy policy, + @Nonnull ConsumerOptions options) { + super(OpenPgpMetadata.getBuilder()); + this.policy = policy; + this.options = options; + this.metadata = new MessageMetadata.Message(); + this.signatures = new Signatures(options); + this.signatures.addDetachedSignatures(options.getDetachedSignatures()); + this.packetInputStream = new TeeBCPGInputStream(BCPGInputStream.wrap(inputStream), signatures); + } + + private static InputStream prepareInputStream(InputStream inputStream, ConsumerOptions options, Policy policy) + throws IOException, PGPException { OpenPgpInputStream openPgpIn = new OpenPgpInputStream(inputStream); openPgpIn.reset(); @@ -131,7 +150,7 @@ public class OpenPgpMessageInputStream extends DecryptionStream { if (openPgpIn.isAsciiArmored()) { ArmoredInputStream armorIn = ArmoredInputStreamFactory.get(openPgpIn); if (armorIn.isClearText()) { - return armorIn; + return parseCleartextSignedMessage(armorIn, options, policy); } else { return armorIn; } @@ -140,6 +159,19 @@ public class OpenPgpMessageInputStream extends DecryptionStream { } } + private static DecryptionStream parseCleartextSignedMessage(ArmoredInputStream armorIn, ConsumerOptions options, Policy policy) + throws IOException, PGPException { + MultiPassStrategy multiPassStrategy = options.getMultiPassStrategy(); + PGPSignatureList signatures = ClearsignedMessageUtil.detachSignaturesFromInbandClearsignedMessage(armorIn, multiPassStrategy.getMessageOutputStream()); + + for (PGPSignature signature : signatures) { + options.addVerificationOfDetachedSignature(signature); + } + + options.forceNonOpenPgpData(); + return new OpenPgpMessageInputStream(multiPassStrategy.getMessageInputStream(), policy, options); + } + /** * Consume OpenPGP packets from the current {@link BCPGInputStream}. * Once an OpenPGP packet with nested data (Literal Data, Compressed Data, Encrypted Data) is reached, @@ -575,17 +607,58 @@ public class OpenPgpMessageInputStream extends DecryptionStream { } } + @Override + public OpenPgpMetadata getResult() { + MessageMetadata m = getMetadata(); + resultBuilder.setCompressionAlgorithm(m.getCompressionAlgorithm()); + resultBuilder.setModificationDate(m.getModificationDate()); + resultBuilder.setFileName(m.getFilename()); + resultBuilder.setFileEncoding(m.getFormat()); + resultBuilder.setSessionKey(m.getSessionKey()); + resultBuilder.setDecryptionKey(m.getDecryptionKey()); + + for (SignatureVerification accepted : m.getVerifiedDetachedSignatures()) { + resultBuilder.addVerifiedDetachedSignature(accepted); + } + for (SignatureVerification.Failure rejected : m.getRejectedDetachedSignatures()) { + resultBuilder.addInvalidDetachedSignature(rejected.getSignatureVerification(), rejected.getValidationException()); + } + + for (SignatureVerification accepted : m.getVerifiedInlineSignatures()) { + resultBuilder.addVerifiedInbandSignature(accepted); + } + for (SignatureVerification.Failure rejected : m.getRejectedInlineSignatures()) { + resultBuilder.addInvalidInbandSignature(rejected.getSignatureVerification(), rejected.getValidationException()); + } + + return resultBuilder.build(); + } + + static void log(String message) { + LOGGER.debug(message); + // CHECKSTYLE:OFF + System.out.println(message); + // CHECKSTYLE:ON + } + + static void log(String message, Throwable e) { + log(message); + // CHECKSTYLE:OFF + e.printStackTrace(); + // CHECKSTYLE:ON + } + // In 'OPS LIT("Foo") SIG', OPS is only updated with "Foo" // In 'OPS[1] OPS LIT("Foo") SIG SIG', OPS[1] (nested) is updated with OPS LIT("Foo") SIG. // Therefore, we need to handle the innermost signature layer differently when updating with Literal data. // Furthermore, For 'OPS COMP(LIT("Foo")) SIG', the signature is updated with "Foo". CHAOS!!! private static final class Signatures extends OutputStream { final ConsumerOptions options; - final List detachedSignatures; - final List prependedSignatures; - final List onePassSignatures; - final Stack> opsUpdateStack; - List literalOPS = new ArrayList<>(); + final List detachedSignatures; + final List prependedSignatures; + final List onePassSignatures; + final Stack> opsUpdateStack; + List literalOPS = new ArrayList<>(); final List correspondingSignatures; boolean isLiteral = true; @@ -605,31 +678,37 @@ public class OpenPgpMessageInputStream extends DecryptionStream { } void addDetachedSignature(PGPSignature signature) { - long keyId = SignatureUtils.determineIssuerKeyId(signature); - PGPPublicKeyRing certificate = findCertificate(keyId); - - if (certificate != null) { - initialize(signature, certificate, keyId); - this.detachedSignatures.add(new DetachedOrPrependedSignature(signature, certificate, keyId)); + SignatureCheck check = initializeSignature(signature); + if (check != null) { + detachedSignatures.add(check); } } void addPrependedSignature(PGPSignature signature) { + SignatureCheck check = initializeSignature(signature); + if (check != null) { + this.prependedSignatures.add(check); + } + } + + SignatureCheck initializeSignature(PGPSignature signature) { long keyId = SignatureUtils.determineIssuerKeyId(signature); PGPPublicKeyRing certificate = findCertificate(keyId); - - if (certificate != null) { - initialize(signature, certificate, keyId); - this.prependedSignatures.add(new DetachedOrPrependedSignature(signature, certificate, keyId)); + if (certificate == null) { + return null; } + + SubkeyIdentifier verifierKey = new SubkeyIdentifier(certificate, keyId); + initialize(signature, certificate, keyId); + return new SignatureCheck(signature, certificate, verifierKey); } void addOnePassSignature(PGPOnePassSignature signature) { PGPPublicKeyRing certificate = findCertificate(signature.getKeyID()); if (certificate != null) { - OnePassSignature ops = new OnePassSignature(signature, certificate, signature.getKeyID()); - ops.init(certificate); + OnePassSignatureCheck ops = new OnePassSignatureCheck(signature, certificate); + initialize(signature, certificate); onePassSignatures.add(ops); literalOPS.add(ops); @@ -641,40 +720,29 @@ public class OpenPgpMessageInputStream extends DecryptionStream { void addCorrespondingOnePassSignature(PGPSignature signature, MessageMetadata.Layer layer, Policy policy) { for (int i = onePassSignatures.size() - 1; i >= 0; i--) { - OnePassSignature onePassSignature = onePassSignatures.get(i); - if (onePassSignature.opSignature.getKeyID() != signature.getKeyID()) { - continue; - } - if (onePassSignature.finished) { + OnePassSignatureCheck onePassSignature = onePassSignatures.get(i); + if (onePassSignature.getOnePassSignature().getKeyID() != signature.getKeyID()) { continue; } - boolean correct = onePassSignature.verify(signature); + if (onePassSignature.getSignature() != null) { + continue; + } + + onePassSignature.setSignature(signature); SignatureVerification verification = new SignatureVerification(signature, - new SubkeyIdentifier(onePassSignature.certificate, onePassSignature.keyId)); - if (correct) { - PGPPublicKey signingKey = onePassSignature.certificate.getPublicKey(onePassSignature.keyId); - try { - checkSignatureValidity(signature, signingKey, policy); - layer.addVerifiedOnePassSignature(verification); - } catch (SignatureValidationException e) { - layer.addRejectedOnePassSignature(new SignatureVerification.Failure(verification, e)); - } - } else { - layer.addRejectedOnePassSignature(new SignatureVerification.Failure(verification, - new SignatureValidationException("Bad Signature."))); + new SubkeyIdentifier(onePassSignature.getVerificationKeys(), onePassSignature.getOnePassSignature().getKeyID())); + + try { + SignatureVerifier.verifyOnePassSignature(signature, onePassSignature.getVerificationKeys().getPublicKey(signature.getKeyID()), onePassSignature, policy); + layer.addVerifiedOnePassSignature(verification); + } catch (SignatureValidationException e) { + layer.addRejectedOnePassSignature(new SignatureVerification.Failure(verification, e)); } break; } } - boolean checkSignatureValidity(PGPSignature signature, PGPPublicKey signingKey, Policy policy) throws SignatureValidationException { - SignatureValidator.wasPossiblyMadeByKey(signingKey).verify(signature); - SignatureValidator.signatureStructureIsAcceptable(signingKey, policy).verify(signature); - SignatureValidator.signatureIsEffective().verify(signature); - return true; - } - void enterNesting() { opsUpdateStack.push(literalOPS); literalOPS = new ArrayList<>(); @@ -718,85 +786,75 @@ public class OpenPgpMessageInputStream extends DecryptionStream { } public void updateLiteral(byte b) { - for (OnePassSignature ops : literalOPS) { - ops.update(b); + for (OnePassSignatureCheck ops : literalOPS) { + ops.getOnePassSignature().update(b); } - for (DetachedOrPrependedSignature detached : detachedSignatures) { - detached.update(b); + for (SignatureCheck detached : detachedSignatures) { + detached.getSignature().update(b); } - for (DetachedOrPrependedSignature prepended : prependedSignatures) { - prepended.update(b); + for (SignatureCheck prepended : prependedSignatures) { + prepended.getSignature().update(b); } } public void updateLiteral(byte[] b, int off, int len) { - for (OnePassSignature ops : literalOPS) { - ops.update(b, off, len); + for (OnePassSignatureCheck ops : literalOPS) { + ops.getOnePassSignature().update(b, off, len); } - for (DetachedOrPrependedSignature detached : detachedSignatures) { - detached.update(b, off, len); + for (SignatureCheck detached : detachedSignatures) { + detached.getSignature().update(b, off, len); } - for (DetachedOrPrependedSignature prepended : prependedSignatures) { - prepended.update(b, off, len); + for (SignatureCheck prepended : prependedSignatures) { + prepended.getSignature().update(b, off, len); } } public void updatePacket(byte b) { for (int i = opsUpdateStack.size() - 1; i >= 0; i--) { - List nestedOPSs = opsUpdateStack.get(i); - for (OnePassSignature ops : nestedOPSs) { - ops.update(b); + List nestedOPSs = opsUpdateStack.get(i); + for (OnePassSignatureCheck ops : nestedOPSs) { + ops.getOnePassSignature().update(b); } } } public void updatePacket(byte[] buf, int off, int len) { for (int i = opsUpdateStack.size() - 1; i >= 0; i--) { - List nestedOPSs = opsUpdateStack.get(i); - for (OnePassSignature ops : nestedOPSs) { - ops.update(buf, off, len); + List nestedOPSs = opsUpdateStack.get(i); + for (OnePassSignatureCheck ops : nestedOPSs) { + ops.getOnePassSignature().update(buf, off, len); } } } public void finish(MessageMetadata.Layer layer, Policy policy) { - for (DetachedOrPrependedSignature detached : detachedSignatures) { - boolean correct = detached.verify(); - SignatureVerification verification = new SignatureVerification( - detached.signature, new SubkeyIdentifier(detached.certificate, detached.keyId)); - if (correct) { - try { - PGPPublicKey signingKey = detached.certificate.getPublicKey(detached.keyId); - checkSignatureValidity(detached.signature, signingKey, policy); - layer.addVerifiedDetachedSignature(verification); - } catch (SignatureValidationException e) { - layer.addRejectedDetachedSignature(new SignatureVerification.Failure(verification, e)); - } - } else { - layer.addRejectedDetachedSignature(new SignatureVerification.Failure( - verification, new SignatureValidationException("Incorrect Signature."))); + for (SignatureCheck detached : detachedSignatures) { + SignatureVerification verification = new SignatureVerification(detached.getSignature(), detached.getSigningKeyIdentifier()); + try { + SignatureVerifier.verifyInitializedSignature( + detached.getSignature(), + detached.getSigningKeyRing().getPublicKey(detached.getSigningKeyIdentifier().getKeyId()), + policy, detached.getSignature().getCreationTime()); + layer.addVerifiedDetachedSignature(verification); + } catch (SignatureValidationException e) { + layer.addRejectedDetachedSignature(new SignatureVerification.Failure(verification, e)); } } - for (DetachedOrPrependedSignature prepended : prependedSignatures) { - boolean correct = prepended.verify(); - SignatureVerification verification = new SignatureVerification( - prepended.signature, new SubkeyIdentifier(prepended.certificate, prepended.keyId)); - if (correct) { - try { - PGPPublicKey signingKey = prepended.certificate.getPublicKey(prepended.keyId); - checkSignatureValidity(prepended.signature, signingKey, policy); - layer.addVerifiedPrependedSignature(verification); - } catch (SignatureValidationException e) { - layer.addRejectedPrependedSignature(new SignatureVerification.Failure(verification, e)); - } - } else { - layer.addRejectedPrependedSignature(new SignatureVerification.Failure( - verification, new SignatureValidationException("Incorrect Signature."))); + for (SignatureCheck prepended : prependedSignatures) { + SignatureVerification verification = new SignatureVerification(prepended.getSignature(), prepended.getSigningKeyIdentifier()); + try { + SignatureVerifier.verifyInitializedSignature( + prepended.getSignature(), + prepended.getSigningKeyRing().getPublicKey(prepended.getSigningKeyIdentifier().getKeyId()), + policy, prepended.getSignature().getCreationTime()); + layer.addVerifiedPrependedSignature(verification); + } catch (SignatureValidationException e) { + layer.addRejectedPrependedSignature(new SignatureVerification.Failure(verification, e)); } } } @@ -822,148 +880,5 @@ public class OpenPgpMessageInputStream extends DecryptionStream { } } - static class DetachedOrPrependedSignature { - ByteArrayOutputStream bytes = new ByteArrayOutputStream(); - PGPSignature signature; - PGPPublicKeyRing certificate; - long keyId; - boolean finished; - boolean valid; - - DetachedOrPrependedSignature(PGPSignature signature, PGPPublicKeyRing certificate, long keyId) { - this.signature = signature; - this.certificate = certificate; - this.keyId = keyId; - } - - public void init(PGPPublicKeyRing certificate) { - initialize(signature, certificate, signature.getKeyID()); - } - - public boolean verify() { - if (finished) { - throw new IllegalStateException("Already finished."); - } - finished = true; - try { - valid = this.signature.verify(); - } catch (PGPException e) { - log("Cannot verify SIG " + signature.getKeyID()); - } - return valid; - } - - public void update(byte b) { - if (finished) { - throw new IllegalStateException("Already finished."); - } - signature.update(b); - bytes.write(b); - } - - public void update(byte[] bytes, int off, int len) { - if (finished) { - throw new IllegalStateException("Already finished."); - } - signature.update(bytes, off, len); - this.bytes.write(bytes, off, len); - } - } - - static class OnePassSignature { - ByteArrayOutputStream bytes = new ByteArrayOutputStream(); - PGPOnePassSignature opSignature; - PGPSignature signature; - PGPPublicKeyRing certificate; - long keyId; - boolean finished; - boolean valid; - - OnePassSignature(PGPOnePassSignature signature, PGPPublicKeyRing certificate, long keyId) { - this.opSignature = signature; - this.certificate = certificate; - this.keyId = keyId; - } - - public void init(PGPPublicKeyRing certificate) { - initialize(opSignature, certificate); - } - - public boolean verify(PGPSignature signature) { - if (finished) { - throw new IllegalStateException("Already finished."); - } - - if (this.opSignature.getKeyID() != signature.getKeyID()) { - // nope - return false; - } - this.signature = signature; - finished = true; - try { - valid = this.opSignature.verify(signature); - } catch (PGPException e) { - log("Cannot verify OPS " + signature.getKeyID()); - } - return valid; - } - - public void update(byte b) { - if (finished) { - throw new IllegalStateException("Already finished."); - } - opSignature.update(b); - bytes.write(b); - } - - public void update(byte[] bytes, int off, int len) { - if (finished) { - throw new IllegalStateException("Already finished."); - } - opSignature.update(bytes, off, len); - this.bytes.write(bytes, off, len); - } - } - } - - @Override - public OpenPgpMetadata getResult() { - MessageMetadata m = getMetadata(); - resultBuilder.setCompressionAlgorithm(m.getCompressionAlgorithm()); - resultBuilder.setModificationDate(m.getModificationDate()); - resultBuilder.setFileName(m.getFilename()); - resultBuilder.setFileEncoding(m.getFormat()); - resultBuilder.setSessionKey(m.getSessionKey()); - resultBuilder.setDecryptionKey(m.getDecryptionKey()); - - for (SignatureVerification accepted : m.getVerifiedDetachedSignatures()) { - resultBuilder.addVerifiedDetachedSignature(accepted); - } - for (SignatureVerification.Failure rejected : m.getRejectedDetachedSignatures()) { - resultBuilder.addInvalidDetachedSignature(rejected.getSignatureVerification(), rejected.getValidationException()); - } - - for (SignatureVerification accepted : m.getVerifiedInlineSignatures()) { - resultBuilder.addVerifiedInbandSignature(accepted); - } - for (SignatureVerification.Failure rejected : m.getRejectedInlineSignatures()) { - resultBuilder.addInvalidInbandSignature(rejected.getSignatureVerification(), rejected.getValidationException()); - } - - return resultBuilder.build(); - } - - static void log(String message) { - LOGGER.debug(message); - // CHECKSTYLE:OFF - System.out.println(message); - // CHECKSTYLE:ON - } - - static void log(String message, Throwable e) { - log(message); - // CHECKSTYLE:OFF - e.printStackTrace(); - // CHECKSTYLE:ON } } 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 275acc17..70a2f4ef 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 @@ -22,7 +22,7 @@ import org.pgpainless.PGPainless; import org.pgpainless.exception.SignatureValidationException; import org.pgpainless.policy.Policy; import org.pgpainless.signature.consumer.CertificateValidator; -import org.pgpainless.signature.consumer.DetachedSignatureCheck; +import org.pgpainless.signature.consumer.SignatureCheck; import org.pgpainless.signature.consumer.OnePassSignatureCheck; import org.pgpainless.signature.SignatureUtils; import org.slf4j.Logger; @@ -41,7 +41,7 @@ public abstract class SignatureInputStream extends FilterInputStream { private final PGPObjectFactory objectFactory; private final List opSignatures; private final Map opSignaturesWithMissingCert; - private final List detachedSignatures; + private final List detachedSignatures; private final ConsumerOptions options; private final OpenPgpMetadata.Builder resultBuilder; @@ -50,7 +50,7 @@ public abstract class SignatureInputStream extends FilterInputStream { @Nullable PGPObjectFactory objectFactory, List opSignatures, Map onePassSignaturesWithMissingCert, - List detachedSignatures, + List detachedSignatures, ConsumerOptions options, OpenPgpMetadata.Builder resultBuilder) { super(literalDataStream); @@ -170,7 +170,7 @@ public abstract class SignatureInputStream extends FilterInputStream { private void verifyDetachedSignatures() { Policy policy = PGPainless.getPolicy(); - for (DetachedSignatureCheck s : detachedSignatures) { + for (SignatureCheck s : detachedSignatures) { try { signatureWasCreatedInBounds(options.getVerifyNotBefore(), options.getVerifyNotAfter()).verify(s.getSignature()); @@ -200,13 +200,13 @@ public abstract class SignatureInputStream extends FilterInputStream { } private void updateDetachedSignatures(byte b) { - for (DetachedSignatureCheck detachedSignature : detachedSignatures) { + for (SignatureCheck detachedSignature : detachedSignatures) { detachedSignature.getSignature().update(b); } } private void updateDetachedSignatures(byte[] b, int off, int read) { - for (DetachedSignatureCheck detachedSignature : detachedSignatures) { + for (SignatureCheck detachedSignature : detachedSignatures) { detachedSignature.getSignature().update(b, off, read); } } diff --git a/pgpainless-core/src/main/java/org/pgpainless/signature/consumer/DetachedSignatureCheck.java b/pgpainless-core/src/main/java/org/pgpainless/signature/consumer/SignatureCheck.java similarity index 90% rename from pgpainless-core/src/main/java/org/pgpainless/signature/consumer/DetachedSignatureCheck.java rename to pgpainless-core/src/main/java/org/pgpainless/signature/consumer/SignatureCheck.java index a431c5de..bc9f1f0b 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/signature/consumer/DetachedSignatureCheck.java +++ b/pgpainless-core/src/main/java/org/pgpainless/signature/consumer/SignatureCheck.java @@ -13,19 +13,19 @@ import org.pgpainless.key.SubkeyIdentifier; * Tuple-class which bundles together a signature, the signing key that created the signature, * an identifier of the signing key and a record of whether the signature was verified. */ -public class DetachedSignatureCheck { +public class SignatureCheck { private final PGPSignature signature; private final PGPKeyRing signingKeyRing; private final SubkeyIdentifier signingKeyIdentifier; /** - * Create a new {@link DetachedSignatureCheck} object. + * Create a new {@link SignatureCheck} object. * * @param signature signature * @param signingKeyRing signing key that created the signature * @param signingKeyIdentifier identifier of the used signing key */ - public DetachedSignatureCheck(PGPSignature signature, PGPKeyRing signingKeyRing, SubkeyIdentifier signingKeyIdentifier) { + public SignatureCheck(PGPSignature signature, PGPKeyRing signingKeyRing, SubkeyIdentifier signingKeyIdentifier) { this.signature = signature; this.signingKeyRing = signingKeyRing; this.signingKeyIdentifier = signingKeyIdentifier; From 0f768a725880ecb4b82b4c7236f7967d48c5f6e1 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 20 Oct 2022 15:35:15 +0200 Subject: [PATCH 34/80] Work on postponed keys --- .../OpenPgpMessageInputStream.java | 121 +++++++++++++++--- 1 file changed, 104 insertions(+), 17 deletions(-) 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 101a60c8..dfb0df91 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 @@ -10,7 +10,9 @@ import java.io.InputStream; import java.io.OutputStream; import java.util.ArrayList; import java.util.Collection; +import java.util.HashSet; import java.util.List; +import java.util.Set; import java.util.Stack; import javax.annotation.Nonnull; @@ -49,6 +51,7 @@ import org.pgpainless.decryption_verification.cleartext_signatures.MultiPassStra import org.pgpainless.exception.MalformedOpenPgpMessageException; import org.pgpainless.exception.MessageNotIntegrityProtectedException; import org.pgpainless.exception.MissingDecryptionMethodException; +import org.pgpainless.exception.MissingPassphraseException; import org.pgpainless.exception.SignatureValidationException; import org.pgpainless.exception.UnacceptableAlgorithmException; import org.pgpainless.implementation.ImplementationFactory; @@ -60,6 +63,7 @@ import org.pgpainless.policy.Policy; import org.pgpainless.signature.SignatureUtils; import org.pgpainless.signature.consumer.OnePassSignatureCheck; import org.pgpainless.signature.consumer.SignatureCheck; +import org.pgpainless.signature.consumer.SignatureValidator; import org.pgpainless.signature.consumer.SignatureVerifier; import org.pgpainless.util.ArmoredInputStreamFactory; import org.pgpainless.util.Passphrase; @@ -127,8 +131,8 @@ public class OpenPgpMessageInputStream extends DecryptionStream { } protected OpenPgpMessageInputStream(@Nonnull InputStream inputStream, - @Nonnull Policy policy, - @Nonnull ConsumerOptions options) { + @Nonnull Policy policy, + @Nonnull ConsumerOptions options) { super(OpenPgpMetadata.getBuilder()); this.policy = policy; this.options = options; @@ -371,6 +375,7 @@ public class OpenPgpMessageInputStream extends DecryptionStream { } } + List> postponedDueToMissingPassphrase = new ArrayList<>(); // Try (known) secret keys for (PGPPublicKeyEncryptedData pkesk : esks.pkesks) { long keyId = pkesk.getKeyID(); @@ -378,7 +383,15 @@ public class OpenPgpMessageInputStream extends DecryptionStream { if (decryptionKeys == null) { continue; } + PGPSecretKey secretKey = decryptionKeys.getSecretKey(keyId); + SecretKeyRingProtector protector = options.getSecretKeyProtector(decryptionKeys); + // Postpone keys with missing passphrase + if (!protector.hasPassphraseFor(keyId)) { + postponedDueToMissingPassphrase.add(new Tuple<>(secretKey, pkesk)); + continue; + } + PGPSecretKey decryptionKey = decryptionKeys.getSecretKey(keyId); PGPPrivateKey privateKey = UnlockSecretKey.unlockSecretKey(decryptionKey, protector); @@ -408,7 +421,12 @@ public class OpenPgpMessageInputStream extends DecryptionStream { // try anonymous secret keys for (PGPPublicKeyEncryptedData pkesk : esks.anonPkesks) { for (Tuple decryptionKeyCandidate : findPotentialDecryptionKeys(pkesk)) { + PGPSecretKey secretKey = decryptionKeyCandidate.getB(); SecretKeyRingProtector protector = options.getSecretKeyProtector(decryptionKeyCandidate.getA()); + if (!protector.hasPassphraseFor(secretKey.getKeyID())) { + postponedDueToMissingPassphrase.add(new Tuple<>(secretKey, pkesk)); + continue; + } PGPPrivateKey privateKey = UnlockSecretKey.unlockSecretKey(decryptionKeyCandidate.getB(), protector); PublicKeyDataDecryptorFactory decryptorFactory = ImplementationFactory.getInstance() .getPublicKeyDataDecryptorFactory(privateKey); @@ -433,10 +451,64 @@ public class OpenPgpMessageInputStream extends DecryptionStream { } } + if (options.getMissingKeyPassphraseStrategy() == MissingKeyPassphraseStrategy.THROW_EXCEPTION) { + // Non-interactive mode: Throw an exception with all locked decryption keys + Set keyIds = new HashSet<>(); + for (Tuple k : postponedDueToMissingPassphrase) { + PGPSecretKey key = k.getA(); + keyIds.add(new SubkeyIdentifier(getDecryptionKey(key.getKeyID()), key.getKeyID())); + } + if (!keyIds.isEmpty()) { + throw new MissingPassphraseException(keyIds); + } + } else if (options.getMissingKeyPassphraseStrategy() == MissingKeyPassphraseStrategy.INTERACTIVE) { + for (PGPPublicKeyEncryptedData pkesk : esks.pkesks) { + // Interactive mode: Fire protector callbacks to get passphrases interactively + for (Tuple missingPassphrases : postponedDueToMissingPassphrase) { + PGPSecretKey secretKey = missingPassphrases.getA(); + long keyId = secretKey.getKeyID(); + PGPSecretKeyRing decryptionKey = getDecryptionKey(keyId); + SecretKeyRingProtector protector = options.getSecretKeyProtector(decryptionKey); + PGPPrivateKey privateKey = UnlockSecretKey.unlockSecretKey(secretKey, protector.getDecryptor(keyId)); + + PublicKeyDataDecryptorFactory decryptorFactory = ImplementationFactory.getInstance() + .getPublicKeyDataDecryptorFactory(privateKey); + + try { + InputStream decrypted = pkesk.getDataStream(decryptorFactory); + SessionKey sessionKey = new SessionKey(pkesk.getSessionKey(decryptorFactory)); + throwIfUnacceptable(sessionKey.getAlgorithm()); + + MessageMetadata.EncryptedData encryptedData = new MessageMetadata.EncryptedData( + SymmetricKeyAlgorithm.requireFromId(pkesk.getSymmetricAlgorithm(decryptorFactory)), + metadata.depth + 1); + encryptedData.decryptionKey = new SubkeyIdentifier(decryptionKey, keyId); + encryptedData.sessionKey = sessionKey; + + IntegrityProtectedInputStream integrityProtected = new IntegrityProtectedInputStream(decrypted, pkesk, options); + nestedInputStream = new OpenPgpMessageInputStream(buffer(integrityProtected), options, encryptedData, policy); + return true; + } catch (PGPException e) { + // hm :/ + } + } + } + } else { + throw new IllegalStateException("Invalid PostponedKeysStrategy set in consumer options."); + } + // we did not yet succeed in decrypting any session key :/ return false; } + private PGPSecretKey getDecryptionKey(PGPSecretKeyRing decryptionKeys, long keyId) { + KeyRingInfo info = PGPainless.inspectKeyRing(decryptionKeys); + if (info.getEncryptionSubkeys(EncryptionPurpose.ANY).contains(info.getPublicKey(keyId))) { + return info.getSecretKey(keyId); + } + return null; + } + private void throwIfUnacceptable(SymmetricKeyAlgorithm algorithm) throws UnacceptableAlgorithmException { if (!policy.getSymmetricKeyDecryptionAlgorithmPolicy().isAcceptable(algorithm)) { @@ -497,10 +569,12 @@ public class OpenPgpMessageInputStream extends DecryptionStream { collectMetadata(); nestedInputStream = null; - try { - consumePackets(); - } catch (PGPException e) { - throw new RuntimeException(e); + if (packetInputStream != null) { + try { + consumePackets(); + } catch (PGPException e) { + throw new RuntimeException(e); + } } signatures.finish(metadata, policy); } @@ -512,23 +586,26 @@ public class OpenPgpMessageInputStream extends DecryptionStream { throws IOException { if (nestedInputStream == null) { - automaton.assertValid(); + if (packetInputStream != null) { + automaton.assertValid(); + } return -1; } int r = nestedInputStream.read(b, off, len); if (r != -1) { signatures.updateLiteral(b, off, r); - } - else { + } else { nestedInputStream.close(); collectMetadata(); nestedInputStream = null; - try { - consumePackets(); - } catch (PGPException e) { - throw new RuntimeException(e); + if (packetInputStream != null) { + try { + consumePackets(); + } catch (PGPException e) { + throw new RuntimeException(e); + } } signatures.finish(metadata, policy); } @@ -539,7 +616,9 @@ public class OpenPgpMessageInputStream extends DecryptionStream { public void close() throws IOException { super.close(); if (closed) { - automaton.assertValid(); + if (packetInputStream != null) { + automaton.assertValid(); + } return; } @@ -555,9 +634,11 @@ public class OpenPgpMessageInputStream extends DecryptionStream { throw new RuntimeException(e); } - automaton.next(InputAlphabet.EndOfSequence); - automaton.assertValid(); - packetInputStream.close(); + if (packetInputStream != null) { + automaton.next(InputAlphabet.EndOfSequence); + automaton.assertValid(); + packetInputStream.close(); + } closed = true; } @@ -734,6 +815,8 @@ public class OpenPgpMessageInputStream extends DecryptionStream { new SubkeyIdentifier(onePassSignature.getVerificationKeys(), onePassSignature.getOnePassSignature().getKeyID())); try { + SignatureValidator.signatureWasCreatedInBounds(options.getVerifyNotBefore(), options.getVerifyNotAfter()) + .verify(signature); SignatureVerifier.verifyOnePassSignature(signature, onePassSignature.getVerificationKeys().getPublicKey(signature.getKeyID()), onePassSignature, policy); layer.addVerifiedOnePassSignature(verification); } catch (SignatureValidationException e) { @@ -835,6 +918,8 @@ public class OpenPgpMessageInputStream extends DecryptionStream { for (SignatureCheck detached : detachedSignatures) { SignatureVerification verification = new SignatureVerification(detached.getSignature(), detached.getSigningKeyIdentifier()); try { + SignatureValidator.signatureWasCreatedInBounds(options.getVerifyNotBefore(), options.getVerifyNotAfter()) + .verify(detached.getSignature()); SignatureVerifier.verifyInitializedSignature( detached.getSignature(), detached.getSigningKeyRing().getPublicKey(detached.getSigningKeyIdentifier().getKeyId()), @@ -848,6 +933,8 @@ public class OpenPgpMessageInputStream extends DecryptionStream { for (SignatureCheck prepended : prependedSignatures) { SignatureVerification verification = new SignatureVerification(prepended.getSignature(), prepended.getSigningKeyIdentifier()); try { + SignatureValidator.signatureWasCreatedInBounds(options.getVerifyNotBefore(), options.getVerifyNotAfter()) + .verify(prepended.getSignature()); SignatureVerifier.verifyInitializedSignature( prepended.getSignature(), prepended.getSigningKeyRing().getPublicKey(prepended.getSigningKeyIdentifier().getKeyId()), From f15040dae0b786a412bc524e4fc5f64e33d2cd7c Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 21 Sep 2022 15:03:45 +0200 Subject: [PATCH 35/80] WIP: Explore Hardware Decryption --- .../ConsumerOptions.java | 6 +++ .../HardwareSecurity.java | 27 +++++++++++++ .../HardwareSecurityCallbackTest.java | 39 +++++++++++++++++++ 3 files changed, 72 insertions(+) create mode 100644 pgpainless-core/src/main/java/org/pgpainless/decryption_verification/HardwareSecurity.java create mode 100644 pgpainless-core/src/test/java/org/pgpainless/decryption_verification/HardwareSecurityCallbackTest.java 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 b6117a60..d57eff48 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 @@ -48,6 +48,7 @@ public class ConsumerOptions { // Session key for decryption without passphrase/key private SessionKey sessionKey = null; + private HardwareSecurity.DecryptionCallback hardwareDecryptionCallback = null; private final Map decryptionKeys = new HashMap<>(); private final Set decryptionPassphrases = new HashSet<>(); @@ -238,6 +239,11 @@ public class ConsumerOptions { return this; } + public ConsumerOptions setHardwareDecryptionCallback(HardwareSecurity.DecryptionCallback callback) { + this.hardwareDecryptionCallback = callback; + return this; + } + public @Nonnull Set getDecryptionKeys() { return Collections.unmodifiableSet(decryptionKeys.keySet()); } diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/HardwareSecurity.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/HardwareSecurity.java new file mode 100644 index 00000000..bea14aef --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/HardwareSecurity.java @@ -0,0 +1,27 @@ +package org.pgpainless.decryption_verification; + +import org.bouncycastle.openpgp.PGPPublicKeyEncryptedData; +import org.pgpainless.util.SessionKey; + +public class HardwareSecurity { + + public interface DecryptionCallback { + + /** + * Delegate decryption of a Public-Key-Encrypted-Session-Key (PKESK) to an external API for dealing with + * hardware security modules such as smartcards or TPMs. + * + * If decryption fails for some reason, a subclass of the {@link HardwareSecurityException} is thrown. + * + * @param pkesk public-key-encrypted session key + * @return decrypted session key + * @throws HardwareSecurityException exception + */ + SessionKey decryptSessionKey(PGPPublicKeyEncryptedData pkesk) throws HardwareSecurityException; + + } + + public static class HardwareSecurityException extends Exception { + + } +} diff --git a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/HardwareSecurityCallbackTest.java b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/HardwareSecurityCallbackTest.java new file mode 100644 index 00000000..d731277e --- /dev/null +++ b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/HardwareSecurityCallbackTest.java @@ -0,0 +1,39 @@ +package org.pgpainless.decryption_verification; + +import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.PGPPublicKeyEncryptedData; +import org.junit.jupiter.api.Test; +import org.pgpainless.PGPainless; +import org.pgpainless.util.SessionKey; + +import java.io.ByteArrayInputStream; +import java.io.IOException; + +public class HardwareSecurityCallbackTest { + + @Test + public void test() throws PGPException, IOException { + PGPainless.decryptAndOrVerify() + .onInputStream(new ByteArrayInputStream(new byte[0])) + .withOptions(ConsumerOptions.get() + .setHardwareDecryptionCallback(new HardwareSecurity.DecryptionCallback() { + @Override + public SessionKey decryptSessionKey(PGPPublicKeyEncryptedData pkesk) throws HardwareSecurity.HardwareSecurityException { + /* + pkesk.getSessionKey(new PublicKeyDataDecryptorFactory() { + @Override + public byte[] recoverSessionData(int keyAlgorithm, byte[][] secKeyData) throws PGPException { + return new byte[0]; + } + + @Override + public PGPDataDecryptor createDataDecryptor(boolean withIntegrityPacket, int encAlgorithm, byte[] key) throws PGPException { + return null; + } + }); + */ + return null; + } + })); + } +} From 0f5103577ede1a8b20bcb7ae71d114a64c244647 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Fri, 23 Sep 2022 16:17:42 +0200 Subject: [PATCH 36/80] Implement exploratory support for custom decryption factories This may enable decryption of messages with hardware-backed keys --- .../ConsumerOptions.java | 24 ++++- .../DecryptionStreamFactory.java | 28 ++++++ .../HardwareSecurity.java | 91 ++++++++++++++++++- ...stomPublicKeyDataDecryptorFactoryTest.java | 86 ++++++++++++++++++ .../HardwareSecurityCallbackTest.java | 39 -------- 5 files changed, 221 insertions(+), 47 deletions(-) create mode 100644 pgpainless-core/src/test/java/org/pgpainless/decryption_verification/CustomPublicKeyDataDecryptorFactoryTest.java delete mode 100644 pgpainless-core/src/test/java/org/pgpainless/decryption_verification/HardwareSecurityCallbackTest.java 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 d57eff48..8f0576ee 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,7 @@ import org.bouncycastle.openpgp.PGPPublicKeyRingCollection; import org.bouncycastle.openpgp.PGPSecretKeyRing; import org.bouncycastle.openpgp.PGPSecretKeyRingCollection; import org.bouncycastle.openpgp.PGPSignature; +import org.bouncycastle.openpgp.operator.PublicKeyDataDecryptorFactory; import org.pgpainless.decryption_verification.cleartext_signatures.InMemoryMultiPassStrategy; import org.pgpainless.decryption_verification.cleartext_signatures.MultiPassStrategy; import org.pgpainless.key.protection.SecretKeyRingProtector; @@ -48,7 +49,7 @@ public class ConsumerOptions { // Session key for decryption without passphrase/key private SessionKey sessionKey = null; - private HardwareSecurity.DecryptionCallback hardwareDecryptionCallback = null; + private Map, PublicKeyDataDecryptorFactory> customPublicKeyDataDecryptorFactories = new HashMap<>(); private final Map decryptionKeys = new HashMap<>(); private final Set decryptionPassphrases = new HashSet<>(); @@ -239,11 +240,28 @@ public class ConsumerOptions { return this; } - public ConsumerOptions setHardwareDecryptionCallback(HardwareSecurity.DecryptionCallback callback) { - this.hardwareDecryptionCallback = callback; + /** + * Add a custom {@link PublicKeyDataDecryptorFactory} which enable decryption of messages, e.g. using + * hardware-backed secret keys. + * (See e.g. {@link org.pgpainless.decryption_verification.HardwareSecurity.HardwareDataDecryptorFactory}). + * + * The set of key-ids determines, whether the decryptor factory shall be consulted to decrypt a given session key. + * See for example {@link HardwareSecurity#getIdsOfHardwareBackedKeys(PGPSecretKeyRing)}. + * + * @param keyIds set of key-ids for which the factory shall be consulted + * @param decryptorFactory decryptor factory + * @return options + */ + public ConsumerOptions addCustomDecryptorFactory( + @Nonnull Set keyIds, @Nonnull PublicKeyDataDecryptorFactory decryptorFactory) { + this.customPublicKeyDataDecryptorFactories.put(keyIds, decryptorFactory); return this; } + Map, PublicKeyDataDecryptorFactory> getCustomDecryptorFactories() { + return new HashMap<>(customPublicKeyDataDecryptorFactories); + } + public @Nonnull Set getDecryptionKeys() { return Collections.unmodifiableSet(decryptionKeys.keySet()); } 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 0739eb19..ba837940 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 @@ -408,6 +408,34 @@ public final class DecryptionStreamFactory { } } + // Try custom PublicKeyDataDecryptorFactories (e.g. hardware-backed). + Map, PublicKeyDataDecryptorFactory> customFactories = options.getCustomDecryptorFactories(); + for (PGPPublicKeyEncryptedData publicKeyEncryptedData : publicKeyProtected) { + Long keyId = publicKeyEncryptedData.getKeyID(); + for (Set keyIds : customFactories.keySet()) { + if (!keyIds.contains(keyId)) { + continue; + } + + PublicKeyDataDecryptorFactory decryptorFactory = customFactories.get(keyIds); + try { + InputStream decryptedDataStream = publicKeyEncryptedData.getDataStream(decryptorFactory); + PGPSessionKey pgpSessionKey = publicKeyEncryptedData.getSessionKey(decryptorFactory); + SessionKey sessionKey = new SessionKey(pgpSessionKey); + resultBuilder.setSessionKey(sessionKey); + + throwIfAlgorithmIsRejected(sessionKey.getAlgorithm()); + + integrityProtectedEncryptedInputStream = + new IntegrityProtectedInputStream(decryptedDataStream, publicKeyEncryptedData, options); + + return integrityProtectedEncryptedInputStream; + } catch (PGPException e) { + LOGGER.debug("Decryption with custom PublicKeyDataDecryptorFactory failed", e); + } + } + } + // Then try decryption with public key encryption for (PGPPublicKeyEncryptedData publicKeyEncryptedData : publicKeyProtected) { PGPPrivateKey privateKey = null; diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/HardwareSecurity.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/HardwareSecurity.java index bea14aef..cc8cf598 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/HardwareSecurity.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/HardwareSecurity.java @@ -1,8 +1,23 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + package org.pgpainless.decryption_verification; -import org.bouncycastle.openpgp.PGPPublicKeyEncryptedData; -import org.pgpainless.util.SessionKey; +import org.bouncycastle.bcpg.S2K; +import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.PGPSecretKey; +import org.bouncycastle.openpgp.PGPSecretKeyRing; +import org.bouncycastle.openpgp.operator.PGPDataDecryptor; +import org.bouncycastle.openpgp.operator.PublicKeyDataDecryptorFactory; +import org.bouncycastle.openpgp.operator.bc.BcPublicKeyDataDecryptorFactory; +import java.util.HashSet; +import java.util.Set; + +/** + * Enable integration of hardware-backed OpenPGP keys. + */ public class HardwareSecurity { public interface DecryptionCallback { @@ -13,15 +28,81 @@ public class HardwareSecurity { * * If decryption fails for some reason, a subclass of the {@link HardwareSecurityException} is thrown. * - * @param pkesk public-key-encrypted session key + * @param keyAlgorithm algorithm + * @param sessionKeyData encrypted session key + * * @return decrypted session key * @throws HardwareSecurityException exception */ - SessionKey decryptSessionKey(PGPPublicKeyEncryptedData pkesk) throws HardwareSecurityException; + byte[] decryptSessionKey(int keyAlgorithm, byte[] sessionKeyData) + throws HardwareSecurityException; } - public static class HardwareSecurityException extends Exception { + /** + * Return the key-ids of all keys which appear to be stored on a hardware token / smartcard. + * + * @param secretKeys secret keys + * @return set of keys with S2K type DIVERT_TO_CARD or GNU_DUMMY_S2K + */ + public static Set getIdsOfHardwareBackedKeys(PGPSecretKeyRing secretKeys) { + Set hardwareBackedKeys = new HashSet<>(); + for (PGPSecretKey secretKey : secretKeys) { + S2K s2K = secretKey.getS2K(); + if (s2K == null) { + continue; + } + + int type = s2K.getType(); + // TODO: Is GNU_DUMMY_S2K appropriate? + if (type == S2K.GNU_PROTECTION_MODE_DIVERT_TO_CARD || type == S2K.GNU_DUMMY_S2K) { + hardwareBackedKeys.add(secretKey.getKeyID()); + } + } + return hardwareBackedKeys; + } + + /** + * Implementation of {@link PublicKeyDataDecryptorFactory} which delegates decryption of encrypted session keys + * to a {@link DecryptionCallback}. + * Users can provide such a callback to delegate decryption of messages to hardware security SDKs. + */ + public static class HardwareDataDecryptorFactory implements PublicKeyDataDecryptorFactory { + + private final DecryptionCallback callback; + // luckily we can instantiate the BcPublicKeyDataDecryptorFactory with null as argument. + private final PublicKeyDataDecryptorFactory factory = + new BcPublicKeyDataDecryptorFactory(null); + + /** + * Create a new {@link HardwareDataDecryptorFactory}. + * + * @param callback decryption callback + */ + public HardwareDataDecryptorFactory(DecryptionCallback callback) { + this.callback = callback; + } + + @Override + public byte[] recoverSessionData(int keyAlgorithm, byte[][] secKeyData) + throws PGPException { + try { + // delegate decryption to the callback + return callback.decryptSessionKey(keyAlgorithm, secKeyData[0]); + } catch (HardwareSecurityException e) { + throw new PGPException("Hardware-backed decryption failed.", e); + } + } + + @Override + public PGPDataDecryptor createDataDecryptor(boolean withIntegrityPacket, int encAlgorithm, byte[] key) + throws PGPException { + return factory.createDataDecryptor(withIntegrityPacket, encAlgorithm, key); + } + } + + public static class HardwareSecurityException + extends Exception { } } diff --git a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/CustomPublicKeyDataDecryptorFactoryTest.java b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/CustomPublicKeyDataDecryptorFactoryTest.java new file mode 100644 index 00000000..f507e02e --- /dev/null +++ b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/CustomPublicKeyDataDecryptorFactoryTest.java @@ -0,0 +1,86 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.decryption_verification; + +import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.PGPPrivateKey; +import org.bouncycastle.openpgp.PGPPublicKey; +import org.bouncycastle.openpgp.PGPPublicKeyRing; +import org.bouncycastle.openpgp.PGPSecretKey; +import org.bouncycastle.openpgp.PGPSecretKeyRing; +import org.bouncycastle.openpgp.operator.PublicKeyDataDecryptorFactory; +import org.bouncycastle.openpgp.operator.bc.BcPublicKeyDataDecryptorFactory; +import org.bouncycastle.util.io.Streams; +import org.junit.jupiter.api.Test; +import org.pgpainless.PGPainless; +import org.pgpainless.algorithm.EncryptionPurpose; +import org.pgpainless.encryption_signing.EncryptionOptions; +import org.pgpainless.encryption_signing.EncryptionStream; +import org.pgpainless.encryption_signing.ProducerOptions; +import org.pgpainless.key.info.KeyRingInfo; +import org.pgpainless.key.protection.UnlockSecretKey; +import org.pgpainless.util.Passphrase; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.security.InvalidAlgorithmParameterException; +import java.security.NoSuchAlgorithmException; +import java.util.Collections; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class CustomPublicKeyDataDecryptorFactoryTest { + + @Test + public void testDecryptionWithEmulatedHardwareDecryptionCallback() + throws PGPException, IOException, InvalidAlgorithmParameterException, NoSuchAlgorithmException { + PGPSecretKeyRing secretKey = PGPainless.generateKeyRing().modernKeyRing("Alice"); + PGPPublicKeyRing cert = PGPainless.extractCertificate(secretKey); + KeyRingInfo info = PGPainless.inspectKeyRing(secretKey); + PGPPublicKey encryptionKey = info.getEncryptionSubkeys(EncryptionPurpose.ANY).get(0); + + // Encrypt a test message + String plaintext = "Hello, World!\n"; + ByteArrayOutputStream ciphertextOut = new ByteArrayOutputStream(); + EncryptionStream encryptionStream = PGPainless.encryptAndOrSign() + .onOutputStream(ciphertextOut) + .withOptions(ProducerOptions.encrypt(EncryptionOptions.get() + .addRecipient(cert))); + encryptionStream.write(plaintext.getBytes(StandardCharsets.UTF_8)); + encryptionStream.close(); + + HardwareSecurity.DecryptionCallback hardwareDecryptionCallback = new HardwareSecurity.DecryptionCallback() { + @Override + public byte[] decryptSessionKey(int keyAlgorithm, byte[] sessionKeyData) + throws HardwareSecurity.HardwareSecurityException { + // Emulate hardware decryption. + try { + PGPSecretKey decryptionKey = secretKey.getSecretKey(encryptionKey.getKeyID()); + PGPPrivateKey privateKey = UnlockSecretKey.unlockSecretKey(decryptionKey, Passphrase.emptyPassphrase()); + PublicKeyDataDecryptorFactory internal = new BcPublicKeyDataDecryptorFactory(privateKey); + return internal.recoverSessionData(keyAlgorithm, new byte[][] {sessionKeyData}); + } catch (PGPException e) { + throw new HardwareSecurity.HardwareSecurityException(); + } + } + }; + + // Decrypt + DecryptionStream decryptionStream = PGPainless.decryptAndOrVerify() + .onInputStream(new ByteArrayInputStream(ciphertextOut.toByteArray())) + .withOptions(ConsumerOptions.get() + .addCustomDecryptorFactory( + Collections.singleton(encryptionKey.getKeyID()), + new HardwareSecurity.HardwareDataDecryptorFactory(hardwareDecryptionCallback))); + + ByteArrayOutputStream decryptedOut = new ByteArrayOutputStream(); + Streams.pipeAll(decryptionStream, decryptedOut); + decryptionStream.close(); + + assertEquals(plaintext, decryptedOut.toString()); + } +} diff --git a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/HardwareSecurityCallbackTest.java b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/HardwareSecurityCallbackTest.java deleted file mode 100644 index d731277e..00000000 --- a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/HardwareSecurityCallbackTest.java +++ /dev/null @@ -1,39 +0,0 @@ -package org.pgpainless.decryption_verification; - -import org.bouncycastle.openpgp.PGPException; -import org.bouncycastle.openpgp.PGPPublicKeyEncryptedData; -import org.junit.jupiter.api.Test; -import org.pgpainless.PGPainless; -import org.pgpainless.util.SessionKey; - -import java.io.ByteArrayInputStream; -import java.io.IOException; - -public class HardwareSecurityCallbackTest { - - @Test - public void test() throws PGPException, IOException { - PGPainless.decryptAndOrVerify() - .onInputStream(new ByteArrayInputStream(new byte[0])) - .withOptions(ConsumerOptions.get() - .setHardwareDecryptionCallback(new HardwareSecurity.DecryptionCallback() { - @Override - public SessionKey decryptSessionKey(PGPPublicKeyEncryptedData pkesk) throws HardwareSecurity.HardwareSecurityException { - /* - pkesk.getSessionKey(new PublicKeyDataDecryptorFactory() { - @Override - public byte[] recoverSessionData(int keyAlgorithm, byte[][] secKeyData) throws PGPException { - return new byte[0]; - } - - @Override - public PGPDataDecryptor createDataDecryptor(boolean withIntegrityPacket, int encAlgorithm, byte[] key) throws PGPException { - return null; - } - }); - */ - return null; - } - })); - } -} From 24ed479b87c50b3a409fea2d4789a16ab842a5f3 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 10 Oct 2022 18:01:30 +0200 Subject: [PATCH 37/80] Change HardwareSecurity DecryptionCallback to emit key-id --- .../ConsumerOptions.java | 15 +++----- .../CustomPublicKeyDataDecryptorFactory.java | 13 +++++++ .../DecryptionStreamFactory.java | 34 +++++++++---------- .../HardwareSecurity.java | 17 +++++++--- ...stomPublicKeyDataDecryptorFactoryTest.java | 8 ++--- 5 files changed, 51 insertions(+), 36 deletions(-) create mode 100644 pgpainless-core/src/main/java/org/pgpainless/decryption_verification/CustomPublicKeyDataDecryptorFactory.java 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 8f0576ee..dba9fca7 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 @@ -49,7 +49,7 @@ public class ConsumerOptions { // Session key for decryption without passphrase/key private SessionKey sessionKey = null; - private Map, PublicKeyDataDecryptorFactory> customPublicKeyDataDecryptorFactories = new HashMap<>(); + private Map customPublicKeyDataDecryptorFactories = new HashMap<>(); private final Map decryptionKeys = new HashMap<>(); private final Set decryptionPassphrases = new HashSet<>(); @@ -245,20 +245,15 @@ public class ConsumerOptions { * hardware-backed secret keys. * (See e.g. {@link org.pgpainless.decryption_verification.HardwareSecurity.HardwareDataDecryptorFactory}). * - * The set of key-ids determines, whether the decryptor factory shall be consulted to decrypt a given session key. - * See for example {@link HardwareSecurity#getIdsOfHardwareBackedKeys(PGPSecretKeyRing)}. - * - * @param keyIds set of key-ids for which the factory shall be consulted - * @param decryptorFactory decryptor factory + * @param factory decryptor factory * @return options */ - public ConsumerOptions addCustomDecryptorFactory( - @Nonnull Set keyIds, @Nonnull PublicKeyDataDecryptorFactory decryptorFactory) { - this.customPublicKeyDataDecryptorFactories.put(keyIds, decryptorFactory); + public ConsumerOptions addCustomDecryptorFactory(@Nonnull CustomPublicKeyDataDecryptorFactory factory) { + this.customPublicKeyDataDecryptorFactories.put(factory.getKeyId(), factory); return this; } - Map, PublicKeyDataDecryptorFactory> getCustomDecryptorFactories() { + Map getCustomDecryptorFactories() { return new HashMap<>(customPublicKeyDataDecryptorFactories); } diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/CustomPublicKeyDataDecryptorFactory.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/CustomPublicKeyDataDecryptorFactory.java new file mode 100644 index 00000000..43e8e723 --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/CustomPublicKeyDataDecryptorFactory.java @@ -0,0 +1,13 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.decryption_verification; + +import org.bouncycastle.openpgp.operator.PublicKeyDataDecryptorFactory; + +public interface CustomPublicKeyDataDecryptorFactory extends PublicKeyDataDecryptorFactory { + + long getKeyId(); + +} 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 ba837940..23e3e47d 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 @@ -409,30 +409,28 @@ public final class DecryptionStreamFactory { } // Try custom PublicKeyDataDecryptorFactories (e.g. hardware-backed). - Map, PublicKeyDataDecryptorFactory> customFactories = options.getCustomDecryptorFactories(); + Map customFactories = options.getCustomDecryptorFactories(); for (PGPPublicKeyEncryptedData publicKeyEncryptedData : publicKeyProtected) { Long keyId = publicKeyEncryptedData.getKeyID(); - for (Set keyIds : customFactories.keySet()) { - if (!keyIds.contains(keyId)) { - continue; - } + if (!customFactories.containsKey(keyId)) { + continue; + } - PublicKeyDataDecryptorFactory decryptorFactory = customFactories.get(keyIds); - try { - InputStream decryptedDataStream = publicKeyEncryptedData.getDataStream(decryptorFactory); - PGPSessionKey pgpSessionKey = publicKeyEncryptedData.getSessionKey(decryptorFactory); - SessionKey sessionKey = new SessionKey(pgpSessionKey); - resultBuilder.setSessionKey(sessionKey); + PublicKeyDataDecryptorFactory decryptorFactory = customFactories.get(keyId); + try { + InputStream decryptedDataStream = publicKeyEncryptedData.getDataStream(decryptorFactory); + PGPSessionKey pgpSessionKey = publicKeyEncryptedData.getSessionKey(decryptorFactory); + SessionKey sessionKey = new SessionKey(pgpSessionKey); + resultBuilder.setSessionKey(sessionKey); - throwIfAlgorithmIsRejected(sessionKey.getAlgorithm()); + throwIfAlgorithmIsRejected(sessionKey.getAlgorithm()); - integrityProtectedEncryptedInputStream = - new IntegrityProtectedInputStream(decryptedDataStream, publicKeyEncryptedData, options); + integrityProtectedEncryptedInputStream = + new IntegrityProtectedInputStream(decryptedDataStream, publicKeyEncryptedData, options); - return integrityProtectedEncryptedInputStream; - } catch (PGPException e) { - LOGGER.debug("Decryption with custom PublicKeyDataDecryptorFactory failed", e); - } + return integrityProtectedEncryptedInputStream; + } catch (PGPException e) { + LOGGER.debug("Decryption with custom PublicKeyDataDecryptorFactory failed", e); } } diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/HardwareSecurity.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/HardwareSecurity.java index cc8cf598..d5bf60db 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/HardwareSecurity.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/HardwareSecurity.java @@ -28,13 +28,14 @@ public class HardwareSecurity { * * If decryption fails for some reason, a subclass of the {@link HardwareSecurityException} is thrown. * + * @param keyId id of the key * @param keyAlgorithm algorithm * @param sessionKeyData encrypted session key * * @return decrypted session key * @throws HardwareSecurityException exception */ - byte[] decryptSessionKey(int keyAlgorithm, byte[] sessionKeyData) + byte[] decryptSessionKey(long keyId, int keyAlgorithm, byte[] sessionKeyData) throws HardwareSecurityException; } @@ -67,20 +68,22 @@ public class HardwareSecurity { * to a {@link DecryptionCallback}. * Users can provide such a callback to delegate decryption of messages to hardware security SDKs. */ - public static class HardwareDataDecryptorFactory implements PublicKeyDataDecryptorFactory { + public static class HardwareDataDecryptorFactory implements CustomPublicKeyDataDecryptorFactory { private final DecryptionCallback callback; // luckily we can instantiate the BcPublicKeyDataDecryptorFactory with null as argument. private final PublicKeyDataDecryptorFactory factory = new BcPublicKeyDataDecryptorFactory(null); + private long keyId; /** * Create a new {@link HardwareDataDecryptorFactory}. * * @param callback decryption callback */ - public HardwareDataDecryptorFactory(DecryptionCallback callback) { + public HardwareDataDecryptorFactory(long keyId, DecryptionCallback callback) { this.callback = callback; + this.keyId = keyId; } @Override @@ -88,7 +91,7 @@ public class HardwareSecurity { throws PGPException { try { // delegate decryption to the callback - return callback.decryptSessionKey(keyAlgorithm, secKeyData[0]); + return callback.decryptSessionKey(keyId, keyAlgorithm, secKeyData[0]); } catch (HardwareSecurityException e) { throw new PGPException("Hardware-backed decryption failed.", e); } @@ -99,10 +102,16 @@ public class HardwareSecurity { throws PGPException { return factory.createDataDecryptor(withIntegrityPacket, encAlgorithm, key); } + + @Override + public long getKeyId() { + return keyId; + } } public static class HardwareSecurityException extends Exception { } + } diff --git a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/CustomPublicKeyDataDecryptorFactoryTest.java b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/CustomPublicKeyDataDecryptorFactoryTest.java index f507e02e..4ea894e5 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/CustomPublicKeyDataDecryptorFactoryTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/CustomPublicKeyDataDecryptorFactoryTest.java @@ -29,7 +29,6 @@ import java.io.IOException; import java.nio.charset.StandardCharsets; import java.security.InvalidAlgorithmParameterException; import java.security.NoSuchAlgorithmException; -import java.util.Collections; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -55,7 +54,7 @@ public class CustomPublicKeyDataDecryptorFactoryTest { HardwareSecurity.DecryptionCallback hardwareDecryptionCallback = new HardwareSecurity.DecryptionCallback() { @Override - public byte[] decryptSessionKey(int keyAlgorithm, byte[] sessionKeyData) + public byte[] decryptSessionKey(long keyId, int keyAlgorithm, byte[] sessionKeyData) throws HardwareSecurity.HardwareSecurityException { // Emulate hardware decryption. try { @@ -74,8 +73,9 @@ public class CustomPublicKeyDataDecryptorFactoryTest { .onInputStream(new ByteArrayInputStream(ciphertextOut.toByteArray())) .withOptions(ConsumerOptions.get() .addCustomDecryptorFactory( - Collections.singleton(encryptionKey.getKeyID()), - new HardwareSecurity.HardwareDataDecryptorFactory(hardwareDecryptionCallback))); + new HardwareSecurity.HardwareDataDecryptorFactory( + encryptionKey.getKeyID(), + hardwareDecryptionCallback))); ByteArrayOutputStream decryptedOut = new ByteArrayOutputStream(); Streams.pipeAll(decryptionStream, decryptedOut); From 02c68460a9fa85f84c7cd8fab79fce5909ed6a3b Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 10 Oct 2022 18:08:43 +0200 Subject: [PATCH 38/80] Make map final --- .../org/pgpainless/decryption_verification/ConsumerOptions.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 dba9fca7..0f02aba0 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 @@ -49,7 +49,7 @@ public class ConsumerOptions { // Session key for decryption without passphrase/key private SessionKey sessionKey = null; - private Map customPublicKeyDataDecryptorFactories = new HashMap<>(); + private final Map customPublicKeyDataDecryptorFactories = new HashMap<>(); private final Map decryptionKeys = new HashMap<>(); private final Set decryptionPassphrases = new HashSet<>(); From a60917549d14f1fe0380628c0b2a6688508822df Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 20 Oct 2022 16:07:31 +0200 Subject: [PATCH 39/80] Identify custom decryptor factories by subkey id --- .../ConsumerOptions.java | 7 ++++--- .../CustomPublicKeyDataDecryptorFactory.java | 3 ++- .../DecryptionStreamFactory.java | 2 +- .../HardwareSecurity.java | 19 +++++++++++++------ ...stomPublicKeyDataDecryptorFactoryTest.java | 3 ++- 5 files changed, 22 insertions(+), 12 deletions(-) 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 0f02aba0..f8f59d36 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 @@ -25,6 +25,7 @@ import org.bouncycastle.openpgp.PGPSignature; import org.bouncycastle.openpgp.operator.PublicKeyDataDecryptorFactory; import org.pgpainless.decryption_verification.cleartext_signatures.InMemoryMultiPassStrategy; import org.pgpainless.decryption_verification.cleartext_signatures.MultiPassStrategy; +import org.pgpainless.key.SubkeyIdentifier; import org.pgpainless.key.protection.SecretKeyRingProtector; import org.pgpainless.signature.SignatureUtils; import org.pgpainless.util.Passphrase; @@ -49,7 +50,7 @@ public class ConsumerOptions { // Session key for decryption without passphrase/key private SessionKey sessionKey = null; - private final Map customPublicKeyDataDecryptorFactories = new HashMap<>(); + private final Map customPublicKeyDataDecryptorFactories = new HashMap<>(); private final Map decryptionKeys = new HashMap<>(); private final Set decryptionPassphrases = new HashSet<>(); @@ -249,11 +250,11 @@ public class ConsumerOptions { * @return options */ public ConsumerOptions addCustomDecryptorFactory(@Nonnull CustomPublicKeyDataDecryptorFactory factory) { - this.customPublicKeyDataDecryptorFactories.put(factory.getKeyId(), factory); + this.customPublicKeyDataDecryptorFactories.put(factory.getSubkeyIdentifier(), factory); return this; } - Map getCustomDecryptorFactories() { + Map getCustomDecryptorFactories() { return new HashMap<>(customPublicKeyDataDecryptorFactories); } diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/CustomPublicKeyDataDecryptorFactory.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/CustomPublicKeyDataDecryptorFactory.java index 43e8e723..37dc10a9 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/CustomPublicKeyDataDecryptorFactory.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/CustomPublicKeyDataDecryptorFactory.java @@ -5,9 +5,10 @@ package org.pgpainless.decryption_verification; import org.bouncycastle.openpgp.operator.PublicKeyDataDecryptorFactory; +import org.pgpainless.key.SubkeyIdentifier; public interface CustomPublicKeyDataDecryptorFactory extends PublicKeyDataDecryptorFactory { - long getKeyId(); + SubkeyIdentifier getSubkeyIdentifier(); } 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 23e3e47d..451750ee 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 @@ -409,7 +409,7 @@ public final class DecryptionStreamFactory { } // Try custom PublicKeyDataDecryptorFactories (e.g. hardware-backed). - Map customFactories = options.getCustomDecryptorFactories(); + Map customFactories = options.getCustomDecryptorFactories(); for (PGPPublicKeyEncryptedData publicKeyEncryptedData : publicKeyProtected) { Long keyId = publicKeyEncryptedData.getKeyID(); if (!customFactories.containsKey(keyId)) { diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/HardwareSecurity.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/HardwareSecurity.java index d5bf60db..f234ff00 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/HardwareSecurity.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/HardwareSecurity.java @@ -11,6 +11,7 @@ import org.bouncycastle.openpgp.PGPSecretKeyRing; import org.bouncycastle.openpgp.operator.PGPDataDecryptor; import org.bouncycastle.openpgp.operator.PublicKeyDataDecryptorFactory; import org.bouncycastle.openpgp.operator.bc.BcPublicKeyDataDecryptorFactory; +import org.pgpainless.key.SubkeyIdentifier; import java.util.HashSet; import java.util.Set; @@ -74,16 +75,16 @@ public class HardwareSecurity { // luckily we can instantiate the BcPublicKeyDataDecryptorFactory with null as argument. private final PublicKeyDataDecryptorFactory factory = new BcPublicKeyDataDecryptorFactory(null); - private long keyId; + private SubkeyIdentifier subkey; /** * Create a new {@link HardwareDataDecryptorFactory}. * * @param callback decryption callback */ - public HardwareDataDecryptorFactory(long keyId, DecryptionCallback callback) { + public HardwareDataDecryptorFactory(SubkeyIdentifier subkeyIdentifier, DecryptionCallback callback) { this.callback = callback; - this.keyId = keyId; + this.subkey = subkeyIdentifier; } @Override @@ -91,7 +92,7 @@ public class HardwareSecurity { throws PGPException { try { // delegate decryption to the callback - return callback.decryptSessionKey(keyId, keyAlgorithm, secKeyData[0]); + return callback.decryptSessionKey(subkey.getSubkeyId(), keyAlgorithm, secKeyData[0]); } catch (HardwareSecurityException e) { throw new PGPException("Hardware-backed decryption failed.", e); } @@ -104,8 +105,14 @@ public class HardwareSecurity { } @Override - public long getKeyId() { - return keyId; + public PGPDataDecryptor createDataDecryptor(int aeadAlgorithm, byte[] iv, int chunkSize, int encAlgorithm, byte[] key) + throws PGPException { + return factory.createDataDecryptor(aeadAlgorithm, iv, chunkSize, encAlgorithm, key); + } + + @Override + public SubkeyIdentifier getSubkeyIdentifier() { + return subkey; } } diff --git a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/CustomPublicKeyDataDecryptorFactoryTest.java b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/CustomPublicKeyDataDecryptorFactoryTest.java index 4ea894e5..73c3bf56 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/CustomPublicKeyDataDecryptorFactoryTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/CustomPublicKeyDataDecryptorFactoryTest.java @@ -19,6 +19,7 @@ import org.pgpainless.algorithm.EncryptionPurpose; import org.pgpainless.encryption_signing.EncryptionOptions; import org.pgpainless.encryption_signing.EncryptionStream; import org.pgpainless.encryption_signing.ProducerOptions; +import org.pgpainless.key.SubkeyIdentifier; import org.pgpainless.key.info.KeyRingInfo; import org.pgpainless.key.protection.UnlockSecretKey; import org.pgpainless.util.Passphrase; @@ -74,7 +75,7 @@ public class CustomPublicKeyDataDecryptorFactoryTest { .withOptions(ConsumerOptions.get() .addCustomDecryptorFactory( new HardwareSecurity.HardwareDataDecryptorFactory( - encryptionKey.getKeyID(), + new SubkeyIdentifier(cert, encryptionKey.getKeyID()), hardwareDecryptionCallback))); ByteArrayOutputStream decryptedOut = new ByteArrayOutputStream(); From 0d62cea8a50e87ca2e3a7c3420d024cc88c905d7 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 20 Oct 2022 16:08:01 +0200 Subject: [PATCH 40/80] Implement custom decryptor factories in pda --- .../OpenPgpMessageInputStream.java | 122 +++++++++--------- 1 file changed, 61 insertions(+), 61 deletions(-) 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 dfb0df91..bba7b396 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 @@ -314,6 +314,20 @@ public class OpenPgpMessageInputStream extends DecryptionStream { SortedESKs esks = new SortedESKs(encDataList); + // Try custom decryptor factory + for (SubkeyIdentifier subkeyIdentifier : options.getCustomDecryptorFactories().keySet()) { + PublicKeyDataDecryptorFactory decryptorFactory = options.getCustomDecryptorFactories().get(subkeyIdentifier); + for (PGPPublicKeyEncryptedData pkesk : esks.pkesks) { + if (pkesk.getKeyID() != subkeyIdentifier.getSubkeyId()) { + continue; + } + + if (decryptPKESKAndStream(subkeyIdentifier, decryptorFactory, pkesk)) { + return true; + } + } + } + // Try session key if (options.getSessionKey() != null) { SessionKey sessionKey = options.getSessionKey(); @@ -357,20 +371,8 @@ public class OpenPgpMessageInputStream extends DecryptionStream { PBEDataDecryptorFactory decryptorFactory = ImplementationFactory.getInstance() .getPBEDataDecryptorFactory(passphrase); - try { - InputStream decrypted = skesk.getDataStream(decryptorFactory); - SessionKey sessionKey = new SessionKey(skesk.getSessionKey(decryptorFactory)); - throwIfUnacceptable(sessionKey.getAlgorithm()); - MessageMetadata.EncryptedData encryptedData = new MessageMetadata.EncryptedData( - sessionKey.getAlgorithm(), metadata.depth + 1); - encryptedData.sessionKey = sessionKey; - IntegrityProtectedInputStream integrityProtected = new IntegrityProtectedInputStream(decrypted, skesk, options); - nestedInputStream = new OpenPgpMessageInputStream(buffer(integrityProtected), options, encryptedData, policy); + if (decryptSKESKAndStream(skesk, decryptorFactory)) { return true; - } catch (UnacceptableAlgorithmException e) { - throw e; - } catch (PGPException e) { - // Password mismatch? } } } @@ -393,28 +395,13 @@ public class OpenPgpMessageInputStream extends DecryptionStream { } PGPSecretKey decryptionKey = decryptionKeys.getSecretKey(keyId); + SubkeyIdentifier decryptionKeyId = new SubkeyIdentifier(decryptionKeys, decryptionKey.getKeyID()); PGPPrivateKey privateKey = UnlockSecretKey.unlockSecretKey(decryptionKey, protector); PublicKeyDataDecryptorFactory decryptorFactory = ImplementationFactory.getInstance() .getPublicKeyDataDecryptorFactory(privateKey); - try { - InputStream decrypted = pkesk.getDataStream(decryptorFactory); - SessionKey sessionKey = new SessionKey(pkesk.getSessionKey(decryptorFactory)); - throwIfUnacceptable(sessionKey.getAlgorithm()); - - MessageMetadata.EncryptedData encryptedData = new MessageMetadata.EncryptedData( - SymmetricKeyAlgorithm.requireFromId(pkesk.getSymmetricAlgorithm(decryptorFactory)), - metadata.depth + 1); - encryptedData.decryptionKey = new SubkeyIdentifier(decryptionKeys, decryptionKey.getKeyID()); - encryptedData.sessionKey = sessionKey; - - IntegrityProtectedInputStream integrityProtected = new IntegrityProtectedInputStream(decrypted, pkesk, options); - nestedInputStream = new OpenPgpMessageInputStream(buffer(integrityProtected), options, encryptedData, policy); + if (decryptPKESKAndStream(decryptionKeyId, decryptorFactory, pkesk)) { return true; - } catch (UnacceptableAlgorithmException e) { - throw e; - } catch (PGPException e) { - } } @@ -430,23 +417,10 @@ public class OpenPgpMessageInputStream extends DecryptionStream { PGPPrivateKey privateKey = UnlockSecretKey.unlockSecretKey(decryptionKeyCandidate.getB(), protector); PublicKeyDataDecryptorFactory decryptorFactory = ImplementationFactory.getInstance() .getPublicKeyDataDecryptorFactory(privateKey); + SubkeyIdentifier decryptionKeyId = new SubkeyIdentifier(decryptionKeyCandidate.getA(), privateKey.getKeyID()); - try { - InputStream decrypted = pkesk.getDataStream(decryptorFactory); - SessionKey sessionKey = new SessionKey(pkesk.getSessionKey(decryptorFactory)); - throwIfUnacceptable(sessionKey.getAlgorithm()); - - MessageMetadata.EncryptedData encryptedData = new MessageMetadata.EncryptedData( - SymmetricKeyAlgorithm.requireFromId(pkesk.getSymmetricAlgorithm(decryptorFactory)), - metadata.depth + 1); - encryptedData.decryptionKey = new SubkeyIdentifier(decryptionKeyCandidate.getA(), privateKey.getKeyID()); - encryptedData.sessionKey = sessionKey; - - IntegrityProtectedInputStream integrityProtected = new IntegrityProtectedInputStream(decrypted, pkesk, options); - nestedInputStream = new OpenPgpMessageInputStream(buffer(integrityProtected), options, encryptedData, policy); + if (decryptPKESKAndStream(decryptionKeyId, decryptorFactory, pkesk)) { return true; - } catch (PGPException e) { - // hm :/ } } } @@ -470,26 +444,12 @@ public class OpenPgpMessageInputStream extends DecryptionStream { PGPSecretKeyRing decryptionKey = getDecryptionKey(keyId); SecretKeyRingProtector protector = options.getSecretKeyProtector(decryptionKey); PGPPrivateKey privateKey = UnlockSecretKey.unlockSecretKey(secretKey, protector.getDecryptor(keyId)); - + SubkeyIdentifier decryptionKeyId = new SubkeyIdentifier(decryptionKey, keyId); PublicKeyDataDecryptorFactory decryptorFactory = ImplementationFactory.getInstance() .getPublicKeyDataDecryptorFactory(privateKey); - try { - InputStream decrypted = pkesk.getDataStream(decryptorFactory); - SessionKey sessionKey = new SessionKey(pkesk.getSessionKey(decryptorFactory)); - throwIfUnacceptable(sessionKey.getAlgorithm()); - - MessageMetadata.EncryptedData encryptedData = new MessageMetadata.EncryptedData( - SymmetricKeyAlgorithm.requireFromId(pkesk.getSymmetricAlgorithm(decryptorFactory)), - metadata.depth + 1); - encryptedData.decryptionKey = new SubkeyIdentifier(decryptionKey, keyId); - encryptedData.sessionKey = sessionKey; - - IntegrityProtectedInputStream integrityProtected = new IntegrityProtectedInputStream(decrypted, pkesk, options); - nestedInputStream = new OpenPgpMessageInputStream(buffer(integrityProtected), options, encryptedData, policy); + if (decryptPKESKAndStream(decryptionKeyId, decryptorFactory, pkesk)) { return true; - } catch (PGPException e) { - // hm :/ } } } @@ -501,6 +461,46 @@ public class OpenPgpMessageInputStream extends DecryptionStream { return false; } + private boolean decryptSKESKAndStream(PGPPBEEncryptedData skesk, PBEDataDecryptorFactory decryptorFactory) throws IOException, UnacceptableAlgorithmException { + try { + InputStream decrypted = skesk.getDataStream(decryptorFactory); + SessionKey sessionKey = new SessionKey(skesk.getSessionKey(decryptorFactory)); + throwIfUnacceptable(sessionKey.getAlgorithm()); + MessageMetadata.EncryptedData encryptedData = new MessageMetadata.EncryptedData( + sessionKey.getAlgorithm(), metadata.depth + 1); + encryptedData.sessionKey = sessionKey; + IntegrityProtectedInputStream integrityProtected = new IntegrityProtectedInputStream(decrypted, skesk, options); + nestedInputStream = new OpenPgpMessageInputStream(buffer(integrityProtected), options, encryptedData, policy); + return true; + } catch (UnacceptableAlgorithmException e) { + throw e; + } catch (PGPException e) { + // Password mismatch? + } + return false; + } + + private boolean decryptPKESKAndStream(SubkeyIdentifier subkeyIdentifier, PublicKeyDataDecryptorFactory decryptorFactory, PGPPublicKeyEncryptedData pkesk) throws IOException { + try { + InputStream decrypted = pkesk.getDataStream(decryptorFactory); + SessionKey sessionKey = new SessionKey(pkesk.getSessionKey(decryptorFactory)); + throwIfUnacceptable(sessionKey.getAlgorithm()); + + MessageMetadata.EncryptedData encryptedData = new MessageMetadata.EncryptedData( + SymmetricKeyAlgorithm.requireFromId(pkesk.getSymmetricAlgorithm(decryptorFactory)), + metadata.depth + 1); + encryptedData.decryptionKey = subkeyIdentifier; + encryptedData.sessionKey = sessionKey; + + IntegrityProtectedInputStream integrityProtected = new IntegrityProtectedInputStream(decrypted, pkesk, options); + nestedInputStream = new OpenPgpMessageInputStream(buffer(integrityProtected), options, encryptedData, policy); + return true; + } catch (PGPException e) { + + } + return false; + } + private PGPSecretKey getDecryptionKey(PGPSecretKeyRing decryptionKeys, long keyId) { KeyRingInfo info = PGPainless.inspectKeyRing(decryptionKeys); if (info.getEncryptionSubkeys(EncryptionPurpose.ANY).contains(info.getPublicKey(keyId))) { From 8fb8aa1a3053ed4c114d291d4fc022f791116059 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 20 Oct 2022 16:13:18 +0200 Subject: [PATCH 41/80] Throw UnacceptableAlgEx for unencrypted encData --- .../decryption_verification/OpenPgpMessageInputStream.java | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) 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 bba7b396..969f3717 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 @@ -480,7 +480,10 @@ public class OpenPgpMessageInputStream extends DecryptionStream { return false; } - private boolean decryptPKESKAndStream(SubkeyIdentifier subkeyIdentifier, PublicKeyDataDecryptorFactory decryptorFactory, PGPPublicKeyEncryptedData pkesk) throws IOException { + private boolean decryptPKESKAndStream(SubkeyIdentifier subkeyIdentifier, + PublicKeyDataDecryptorFactory decryptorFactory, + PGPPublicKeyEncryptedData pkesk) + throws IOException, UnacceptableAlgorithmException { try { InputStream decrypted = pkesk.getDataStream(decryptorFactory); SessionKey sessionKey = new SessionKey(pkesk.getSessionKey(decryptorFactory)); @@ -495,6 +498,8 @@ public class OpenPgpMessageInputStream extends DecryptionStream { IntegrityProtectedInputStream integrityProtected = new IntegrityProtectedInputStream(decrypted, pkesk, options); nestedInputStream = new OpenPgpMessageInputStream(buffer(integrityProtected), options, encryptedData, policy); return true; + } catch (UnacceptableAlgorithmException e) { + throw e; } catch (PGPException e) { } From 789a0086c95bb5f4a4500d74868f414ab2829818 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 20 Oct 2022 19:35:25 +0200 Subject: [PATCH 42/80] Enable logging in tests --- pgpainless-core/src/test/resources/logback-test.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pgpainless-core/src/test/resources/logback-test.xml b/pgpainless-core/src/test/resources/logback-test.xml index 7e4c3194..bb16e293 100644 --- a/pgpainless-core/src/test/resources/logback-test.xml +++ b/pgpainless-core/src/test/resources/logback-test.xml @@ -19,7 +19,7 @@ SPDX-License-Identifier: Apache-2.0 - + From f67f1954e7c14695cacf95fadf29ae611e574308 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 20 Oct 2022 19:35:50 +0200 Subject: [PATCH 43/80] Add detailled logging to OpenPgpMessageInputStream --- .../OpenPgpMessageInputStream.java | 84 +++++++++++++++---- .../automaton/PDA.java | 13 ++- .../org/pgpainless/key/util/KeyIdUtil.java | 4 + 3 files changed, 82 insertions(+), 19 deletions(-) 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 969f3717..1dbadbbe 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 @@ -59,6 +59,7 @@ import org.pgpainless.key.SubkeyIdentifier; import org.pgpainless.key.info.KeyRingInfo; import org.pgpainless.key.protection.SecretKeyRingProtector; import org.pgpainless.key.protection.UnlockSecretKey; +import org.pgpainless.key.util.KeyIdUtil; import org.pgpainless.policy.Policy; import org.pgpainless.signature.SignatureUtils; import org.pgpainless.signature.consumer.OnePassSignatureCheck; @@ -234,6 +235,7 @@ public class OpenPgpMessageInputStream extends DecryptionStream { // Marker Packets need to be skipped and ignored case MARKER: + LOGGER.debug("Skipping Marker Packet"); packetInputStream.readMarker(); break; @@ -263,6 +265,7 @@ public class OpenPgpMessageInputStream extends DecryptionStream { } private void processLiteralData() throws IOException { + LOGGER.debug("Literal Data Packet at depth " + metadata.depth + " encountered"); automaton.next(InputAlphabet.LiteralData); PGPLiteralData literalData = packetInputStream.readLiteralData(); this.metadata.setChild(new MessageMetadata.LiteralData( @@ -279,6 +282,7 @@ public class OpenPgpMessageInputStream extends DecryptionStream { MessageMetadata.CompressedData compressionLayer = new MessageMetadata.CompressedData( CompressionAlgorithm.fromId(compressedData.getAlgorithm()), metadata.depth + 1); + LOGGER.debug("Compressed Data Packet (" + compressionLayer.algorithm + ") at depth " + metadata.depth + " encountered"); InputStream decompressed = compressedData.getDataStream(); nestedInputStream = new OpenPgpMessageInputStream(buffer(decompressed), options, compressionLayer, policy); } @@ -286,6 +290,8 @@ public class OpenPgpMessageInputStream extends DecryptionStream { private void processOnePassSignature() throws PGPException, IOException { automaton.next(InputAlphabet.OnePassSignature); PGPOnePassSignature onePassSignature = packetInputStream.readOnePassSignature(); + LOGGER.debug("One-Pass-Signature Packet by key " + KeyIdUtil.formatKeyId(onePassSignature.getKeyID()) + + "at depth " + metadata.depth + " encountered"); signatures.addOnePassSignature(onePassSignature); } @@ -294,42 +300,59 @@ public class OpenPgpMessageInputStream extends DecryptionStream { boolean isSigForOPS = automaton.peekStack() == StackAlphabet.ops; automaton.next(InputAlphabet.Signature); PGPSignature signature = packetInputStream.readSignature(); + long keyId = SignatureUtils.determineIssuerKeyId(signature); if (isSigForOPS) { + LOGGER.debug("Signature Packet corresponding to One-Pass-Signature by key " + + KeyIdUtil.formatKeyId(keyId) + + " at depth " + metadata.depth + " encountered"); signatures.leaveNesting(); // TODO: Only leave nesting if all OPSs of the nesting layer are dealt with signatures.addCorrespondingOnePassSignature(signature, metadata, policy); } else { + LOGGER.debug("Prepended Signature Packet by key " + + KeyIdUtil.formatKeyId(keyId) + + " at depth " + metadata.depth + " encountered"); signatures.addPrependedSignature(signature); } } private boolean processEncryptedData() throws IOException, PGPException { + LOGGER.debug("Symmetrically Encrypted Data Packet at depth " + metadata.depth + " encountered"); automaton.next(InputAlphabet.EncryptedData); PGPEncryptedDataList encDataList = packetInputStream.readEncryptedDataList(); // TODO: Replace with !encDataList.isIntegrityProtected() // once BC ships it if (!encDataList.get(0).isIntegrityProtected()) { + LOGGER.debug("Symmetrically Encrypted Data Packet is not integrity-protected and is therefore rejected."); throw new MessageNotIntegrityProtectedException(); } SortedESKs esks = new SortedESKs(encDataList); + LOGGER.debug("Symmetrically Encrypted Integrity-Protected Data has " + + esks.skesks.size() + " SKESK(s) and " + + (esks.pkesks.size() + esks.anonPkesks.size()) + " PKESK(s) from which " + + esks.anonPkesks.size() + " PKESK(s) have an anonymous recipient"); - // Try custom decryptor factory + // Try custom decryptor factories for (SubkeyIdentifier subkeyIdentifier : options.getCustomDecryptorFactories().keySet()) { + LOGGER.debug("Attempt decryption with custom decryptor factory with key " + subkeyIdentifier); PublicKeyDataDecryptorFactory decryptorFactory = options.getCustomDecryptorFactories().get(subkeyIdentifier); for (PGPPublicKeyEncryptedData pkesk : esks.pkesks) { + // find matching PKESK if (pkesk.getKeyID() != subkeyIdentifier.getSubkeyId()) { continue; } + // attempt decryption if (decryptPKESKAndStream(subkeyIdentifier, decryptorFactory, pkesk)) { return true; } } } - // Try session key + // Try provided session key if (options.getSessionKey() != null) { + LOGGER.debug("Attempt decryption with provided session key"); SessionKey sessionKey = options.getSessionKey(); throwIfUnacceptable(sessionKey.getAlgorithm()); @@ -340,6 +363,7 @@ public class OpenPgpMessageInputStream extends DecryptionStream { try { // TODO: Use BCs new API once shipped + // see https://github.com/bcgit/bc-java/pull/1228 (discussion) PGPEncryptedData esk = esks.all().get(0); if (esk instanceof PGPPBEEncryptedData) { PGPPBEEncryptedData skesk = (PGPPBEEncryptedData) esk; @@ -347,6 +371,7 @@ public class OpenPgpMessageInputStream extends DecryptionStream { encryptedData.sessionKey = sessionKey; IntegrityProtectedInputStream integrityProtected = new IntegrityProtectedInputStream(decrypted, skesk, options); nestedInputStream = new OpenPgpMessageInputStream(buffer(integrityProtected), options, encryptedData, policy); + LOGGER.debug("Successfully decrypted data with provided session key"); return true; } else if (esk instanceof PGPPublicKeyEncryptedData) { PGPPublicKeyEncryptedData pkesk = (PGPPublicKeyEncryptedData) esk; @@ -354,20 +379,29 @@ public class OpenPgpMessageInputStream extends DecryptionStream { encryptedData.sessionKey = sessionKey; IntegrityProtectedInputStream integrityProtected = new IntegrityProtectedInputStream(decrypted, pkesk, options); nestedInputStream = new OpenPgpMessageInputStream(buffer(integrityProtected), options, encryptedData, policy); + LOGGER.debug("Successfully decrypted data with provided session key"); return true; } else { throw new RuntimeException("Unknown ESK class type: " + esk.getClass().getName()); } } catch (PGPException e) { // Session key mismatch? + LOGGER.debug("Decryption using provided session key failed. Mismatched session key and message?", e); } } // Try passwords - for (PGPPBEEncryptedData skesk : esks.skesks) { - SymmetricKeyAlgorithm kekAlgorithm = SymmetricKeyAlgorithm.requireFromId(skesk.getAlgorithm()); - throwIfUnacceptable(kekAlgorithm); - for (Passphrase passphrase : options.getDecryptionPassphrases()) { + for (Passphrase passphrase : options.getDecryptionPassphrases()) { + for (PGPPBEEncryptedData skesk : esks.skesks) { + LOGGER.debug("Attempt decryption with provided passphrase"); + SymmetricKeyAlgorithm kekAlgorithm = SymmetricKeyAlgorithm.requireFromId(skesk.getAlgorithm()); + try { + throwIfUnacceptable(kekAlgorithm); + } catch (UnacceptableAlgorithmException e) { + LOGGER.debug("Skipping SKESK with unacceptable encapsulation algorithm", e); + continue; + } + PBEDataDecryptorFactory decryptorFactory = ImplementationFactory.getInstance() .getPBEDataDecryptorFactory(passphrase); @@ -381,22 +415,25 @@ public class OpenPgpMessageInputStream extends DecryptionStream { // Try (known) secret keys for (PGPPublicKeyEncryptedData pkesk : esks.pkesks) { long keyId = pkesk.getKeyID(); + LOGGER.debug("Encountered PKESK with recipient " + KeyIdUtil.formatKeyId(keyId)); PGPSecretKeyRing decryptionKeys = getDecryptionKey(keyId); if (decryptionKeys == null) { + LOGGER.debug("Skipping PKESK because no matching key " + KeyIdUtil.formatKeyId(keyId) + " was provided"); continue; } PGPSecretKey secretKey = decryptionKeys.getSecretKey(keyId); + SubkeyIdentifier decryptionKeyId = new SubkeyIdentifier(decryptionKeys, secretKey.getKeyID()); + LOGGER.debug("Attempt decryption using secret key " + decryptionKeyId); SecretKeyRingProtector protector = options.getSecretKeyProtector(decryptionKeys); // Postpone keys with missing passphrase if (!protector.hasPassphraseFor(keyId)) { + LOGGER.debug("Missing passphrase for key " + decryptionKeyId + ". Postponing decryption until all other keys were tried"); postponedDueToMissingPassphrase.add(new Tuple<>(secretKey, pkesk)); continue; } - PGPSecretKey decryptionKey = decryptionKeys.getSecretKey(keyId); - SubkeyIdentifier decryptionKeyId = new SubkeyIdentifier(decryptionKeys, decryptionKey.getKeyID()); - PGPPrivateKey privateKey = UnlockSecretKey.unlockSecretKey(decryptionKey, protector); + PGPPrivateKey privateKey = UnlockSecretKey.unlockSecretKey(secretKey, protector); PublicKeyDataDecryptorFactory decryptorFactory = ImplementationFactory.getInstance() .getPublicKeyDataDecryptorFactory(privateKey); @@ -408,16 +445,19 @@ public class OpenPgpMessageInputStream extends DecryptionStream { // try anonymous secret keys for (PGPPublicKeyEncryptedData pkesk : esks.anonPkesks) { for (Tuple decryptionKeyCandidate : findPotentialDecryptionKeys(pkesk)) { + PGPSecretKeyRing decryptionKeys = decryptionKeyCandidate.getA(); PGPSecretKey secretKey = decryptionKeyCandidate.getB(); + SubkeyIdentifier decryptionKeyId = new SubkeyIdentifier(decryptionKeys, secretKey.getKeyID()); + LOGGER.debug("Attempt decryption of anonymous PKESK with key " + decryptionKeyId); SecretKeyRingProtector protector = options.getSecretKeyProtector(decryptionKeyCandidate.getA()); if (!protector.hasPassphraseFor(secretKey.getKeyID())) { + LOGGER.debug("Missing passphrase for key " + decryptionKeyId + ". Postponing decryption until all other keys were tried."); postponedDueToMissingPassphrase.add(new Tuple<>(secretKey, pkesk)); continue; } PGPPrivateKey privateKey = UnlockSecretKey.unlockSecretKey(decryptionKeyCandidate.getB(), protector); PublicKeyDataDecryptorFactory decryptorFactory = ImplementationFactory.getInstance() .getPublicKeyDataDecryptorFactory(privateKey); - SubkeyIdentifier decryptionKeyId = new SubkeyIdentifier(decryptionKeyCandidate.getA(), privateKey.getKeyID()); if (decryptPKESKAndStream(decryptionKeyId, decryptorFactory, pkesk)) { return true; @@ -442,9 +482,10 @@ public class OpenPgpMessageInputStream extends DecryptionStream { PGPSecretKey secretKey = missingPassphrases.getA(); long keyId = secretKey.getKeyID(); PGPSecretKeyRing decryptionKey = getDecryptionKey(keyId); + SubkeyIdentifier decryptionKeyId = new SubkeyIdentifier(decryptionKey, keyId); + LOGGER.debug("Attempt decryption with key " + decryptionKeyId + " while interactively requesting its passphrase"); SecretKeyRingProtector protector = options.getSecretKeyProtector(decryptionKey); PGPPrivateKey privateKey = UnlockSecretKey.unlockSecretKey(secretKey, protector.getDecryptor(keyId)); - SubkeyIdentifier decryptionKeyId = new SubkeyIdentifier(decryptionKey, keyId); PublicKeyDataDecryptorFactory decryptorFactory = ImplementationFactory.getInstance() .getPublicKeyDataDecryptorFactory(privateKey); @@ -458,10 +499,13 @@ public class OpenPgpMessageInputStream extends DecryptionStream { } // we did not yet succeed in decrypting any session key :/ + + LOGGER.debug("Failed to decrypt encrypted data packet"); return false; } - private boolean decryptSKESKAndStream(PGPPBEEncryptedData skesk, PBEDataDecryptorFactory decryptorFactory) throws IOException, UnacceptableAlgorithmException { + private boolean decryptSKESKAndStream(PGPPBEEncryptedData skesk, PBEDataDecryptorFactory decryptorFactory) + throws IOException, UnacceptableAlgorithmException { try { InputStream decrypted = skesk.getDataStream(decryptorFactory); SessionKey sessionKey = new SessionKey(skesk.getSessionKey(decryptorFactory)); @@ -469,18 +513,19 @@ public class OpenPgpMessageInputStream extends DecryptionStream { MessageMetadata.EncryptedData encryptedData = new MessageMetadata.EncryptedData( sessionKey.getAlgorithm(), metadata.depth + 1); encryptedData.sessionKey = sessionKey; + LOGGER.debug("Successfully decrypted data with passphrase"); IntegrityProtectedInputStream integrityProtected = new IntegrityProtectedInputStream(decrypted, skesk, options); nestedInputStream = new OpenPgpMessageInputStream(buffer(integrityProtected), options, encryptedData, policy); return true; } catch (UnacceptableAlgorithmException e) { throw e; } catch (PGPException e) { - // Password mismatch? + LOGGER.debug("Decryption of encrypted data packet using password failed. Password mismatch?", e); } return false; } - private boolean decryptPKESKAndStream(SubkeyIdentifier subkeyIdentifier, + private boolean decryptPKESKAndStream(SubkeyIdentifier decryptionKeyId, PublicKeyDataDecryptorFactory decryptorFactory, PGPPublicKeyEncryptedData pkesk) throws IOException, UnacceptableAlgorithmException { @@ -492,16 +537,17 @@ public class OpenPgpMessageInputStream extends DecryptionStream { MessageMetadata.EncryptedData encryptedData = new MessageMetadata.EncryptedData( SymmetricKeyAlgorithm.requireFromId(pkesk.getSymmetricAlgorithm(decryptorFactory)), metadata.depth + 1); - encryptedData.decryptionKey = subkeyIdentifier; + encryptedData.decryptionKey = decryptionKeyId; encryptedData.sessionKey = sessionKey; + LOGGER.debug("Successfully decrypted data with key " + decryptionKeyId); IntegrityProtectedInputStream integrityProtected = new IntegrityProtectedInputStream(decrypted, pkesk, options); nestedInputStream = new OpenPgpMessageInputStream(buffer(integrityProtected), options, encryptedData, policy); return true; } catch (UnacceptableAlgorithmException e) { throw e; } catch (PGPException e) { - + LOGGER.debug("Decryption of encrypted data packet using secret key failed.", e); } return false; } @@ -823,8 +869,10 @@ public class OpenPgpMessageInputStream extends DecryptionStream { SignatureValidator.signatureWasCreatedInBounds(options.getVerifyNotBefore(), options.getVerifyNotAfter()) .verify(signature); SignatureVerifier.verifyOnePassSignature(signature, onePassSignature.getVerificationKeys().getPublicKey(signature.getKeyID()), onePassSignature, policy); + LOGGER.debug("Acceptable signature by key " + verification.getSigningKey()); layer.addVerifiedOnePassSignature(verification); } catch (SignatureValidationException e) { + LOGGER.debug("Rejected signature by key " + verification.getSigningKey(), e); layer.addRejectedOnePassSignature(new SignatureVerification.Failure(verification, e)); } break; @@ -929,8 +977,10 @@ public class OpenPgpMessageInputStream extends DecryptionStream { detached.getSignature(), detached.getSigningKeyRing().getPublicKey(detached.getSigningKeyIdentifier().getKeyId()), policy, detached.getSignature().getCreationTime()); + LOGGER.debug("Acceptable signature by key " + verification.getSigningKey()); layer.addVerifiedDetachedSignature(verification); } catch (SignatureValidationException e) { + LOGGER.debug("Rejected signature by key " + verification.getSigningKey(), e); layer.addRejectedDetachedSignature(new SignatureVerification.Failure(verification, e)); } } @@ -944,8 +994,10 @@ public class OpenPgpMessageInputStream extends DecryptionStream { prepended.getSignature(), prepended.getSigningKeyRing().getPublicKey(prepended.getSigningKeyIdentifier().getKeyId()), policy, prepended.getSignature().getCreationTime()); + LOGGER.debug("Acceptable signature by key " + verification.getSigningKey()); layer.addVerifiedPrependedSignature(verification); } catch (SignatureValidationException e) { + LOGGER.debug("Rejected signature by key " + verification.getSigningKey(), e); layer.addRejectedPrependedSignature(new SignatureVerification.Failure(verification, e)); } } 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 156dc2ed..3df3c5b5 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 @@ -5,6 +5,8 @@ package org.pgpainless.decryption_verification.automaton; import org.pgpainless.exception.MalformedOpenPgpMessageException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import java.util.Stack; @@ -15,11 +17,11 @@ import static org.pgpainless.decryption_verification.automaton.StackAlphabet.ter public class PDA { private static int ID = 0; + private static final Logger LOGGER = LoggerFactory.getLogger(PDA.class); /** * Set of states of the automaton. - * Each state defines its valid transitions in their {@link NestingPDA.State#transition(InputAlphabet, NestingPDA)} - * method. + * Each state defines its valid transitions in their {@link State#transition(InputAlphabet, PDA)} method. */ public enum State { @@ -199,7 +201,12 @@ public class PDA { } public void next(InputAlphabet input) throws MalformedOpenPgpMessageException { - state = state.transition(input, this); + try { + state = state.transition(input, this); + } catch (MalformedOpenPgpMessageException e) { + LOGGER.debug("Unexpected Packet or Token '" + input + "' encountered. Message is malformed.", e); + throw e; + } } /** diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/util/KeyIdUtil.java b/pgpainless-core/src/main/java/org/pgpainless/key/util/KeyIdUtil.java index e6ab4f48..7ebac8fd 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/key/util/KeyIdUtil.java +++ b/pgpainless-core/src/main/java/org/pgpainless/key/util/KeyIdUtil.java @@ -30,4 +30,8 @@ public final class KeyIdUtil { return new BigInteger(longKeyId, 16).longValue(); } + + public static String formatKeyId(long keyId) { + return Long.toHexString(keyId).toUpperCase(); + } } From 9dbee6730401c01a59bbd49512096ce5609302d5 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Fri, 21 Oct 2022 00:31:25 +0200 Subject: [PATCH 44/80] Rename automaton package to syntax_check --- .../OpenPgpMessageInputStream.java | 33 ++++++++++--------- .../InputAlphabet.java | 2 +- .../{automaton => syntax_check}/PDA.java | 8 ++--- .../StackAlphabet.java | 2 +- .../package-info.java | 2 +- .../MalformedOpenPgpMessageException.java | 6 ++-- .../{automaton => syntax_check}/PDATest.java | 2 +- 7 files changed, 28 insertions(+), 27 deletions(-) rename pgpainless-core/src/main/java/org/pgpainless/decryption_verification/{automaton => syntax_check}/InputAlphabet.java (95%) rename pgpainless-core/src/main/java/org/pgpainless/decryption_verification/{automaton => syntax_check}/PDA.java (96%) rename pgpainless-core/src/main/java/org/pgpainless/decryption_verification/{automaton => syntax_check}/StackAlphabet.java (89%) rename pgpainless-core/src/main/java/org/pgpainless/decryption_verification/{automaton => syntax_check}/package-info.java (78%) rename pgpainless-core/src/test/java/org/pgpainless/decryption_verification/{automaton => syntax_check}/PDATest.java (97%) 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 1dbadbbe..a2fa2351 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 @@ -43,9 +43,9 @@ 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; -import org.pgpainless.decryption_verification.automaton.StackAlphabet; +import org.pgpainless.decryption_verification.syntax_check.InputAlphabet; +import org.pgpainless.decryption_verification.syntax_check.PDA; +import org.pgpainless.decryption_verification.syntax_check.StackAlphabet; import org.pgpainless.decryption_verification.cleartext_signatures.ClearsignedMessageUtil; import org.pgpainless.decryption_verification.cleartext_signatures.MultiPassStrategy; import org.pgpainless.exception.MalformedOpenPgpMessageException; @@ -80,7 +80,7 @@ public class OpenPgpMessageInputStream extends DecryptionStream { // Options to consume the data protected final ConsumerOptions options; // Pushdown Automaton to verify validity of OpenPGP packet sequence in an OpenPGP message - protected final PDA automaton = new PDA(); + protected final PDA syntaxVerifier = new PDA(); // InputStream of OpenPGP packets protected TeeBCPGInputStream packetInputStream; // InputStream of a nested data packet @@ -266,7 +266,7 @@ public class OpenPgpMessageInputStream extends DecryptionStream { private void processLiteralData() throws IOException { LOGGER.debug("Literal Data Packet at depth " + metadata.depth + " encountered"); - automaton.next(InputAlphabet.LiteralData); + syntaxVerifier.next(InputAlphabet.LiteralData); PGPLiteralData literalData = packetInputStream.readLiteralData(); this.metadata.setChild(new MessageMetadata.LiteralData( literalData.getFileName(), @@ -276,7 +276,7 @@ public class OpenPgpMessageInputStream extends DecryptionStream { } private void processCompressedData() throws IOException, PGPException { - automaton.next(InputAlphabet.CompressedData); + syntaxVerifier.next(InputAlphabet.CompressedData); signatures.enterNesting(); PGPCompressedData compressedData = packetInputStream.readCompressedData(); MessageMetadata.CompressedData compressionLayer = new MessageMetadata.CompressedData( @@ -288,7 +288,7 @@ public class OpenPgpMessageInputStream extends DecryptionStream { } private void processOnePassSignature() throws PGPException, IOException { - automaton.next(InputAlphabet.OnePassSignature); + syntaxVerifier.next(InputAlphabet.OnePassSignature); PGPOnePassSignature onePassSignature = packetInputStream.readOnePassSignature(); LOGGER.debug("One-Pass-Signature Packet by key " + KeyIdUtil.formatKeyId(onePassSignature.getKeyID()) + "at depth " + metadata.depth + " encountered"); @@ -297,8 +297,8 @@ public class OpenPgpMessageInputStream extends DecryptionStream { private void processSignature() throws PGPException, IOException { // true if Signature corresponds to OnePassSignature - boolean isSigForOPS = automaton.peekStack() == StackAlphabet.ops; - automaton.next(InputAlphabet.Signature); + boolean isSigForOPS = syntaxVerifier.peekStack() == StackAlphabet.ops; + syntaxVerifier.next(InputAlphabet.Signature); PGPSignature signature = packetInputStream.readSignature(); long keyId = SignatureUtils.determineIssuerKeyId(signature); if (isSigForOPS) { @@ -317,7 +317,7 @@ public class OpenPgpMessageInputStream extends DecryptionStream { private boolean processEncryptedData() throws IOException, PGPException { LOGGER.debug("Symmetrically Encrypted Data Packet at depth " + metadata.depth + " encountered"); - automaton.next(InputAlphabet.EncryptedData); + syntaxVerifier.next(InputAlphabet.EncryptedData); PGPEncryptedDataList encDataList = packetInputStream.readEncryptedDataList(); // TODO: Replace with !encDataList.isIntegrityProtected() @@ -601,7 +601,7 @@ public class OpenPgpMessageInputStream extends DecryptionStream { @Override public int read() throws IOException { if (nestedInputStream == null) { - automaton.assertValid(); + syntaxVerifier.assertValid(); return -1; } @@ -638,7 +638,7 @@ public class OpenPgpMessageInputStream extends DecryptionStream { if (nestedInputStream == null) { if (packetInputStream != null) { - automaton.assertValid(); + syntaxVerifier.assertValid(); } return -1; } @@ -668,7 +668,7 @@ public class OpenPgpMessageInputStream extends DecryptionStream { super.close(); if (closed) { if (packetInputStream != null) { - automaton.assertValid(); + syntaxVerifier.assertValid(); } return; } @@ -686,8 +686,8 @@ public class OpenPgpMessageInputStream extends DecryptionStream { } if (packetInputStream != null) { - automaton.next(InputAlphabet.EndOfSequence); - automaton.assertValid(); + syntaxVerifier.next(InputAlphabet.EndOfSequence); + syntaxVerifier.assertValid(); packetInputStream.close(); } closed = true; @@ -717,7 +717,8 @@ public class OpenPgpMessageInputStream extends DecryptionStream { for (PGPEncryptedData esk : esks) { if (esk instanceof PGPPBEEncryptedData) { skesks.add((PGPPBEEncryptedData) esk); - } else if (esk instanceof PGPPublicKeyEncryptedData) { + } + else if (esk instanceof PGPPublicKeyEncryptedData) { PGPPublicKeyEncryptedData pkesk = (PGPPublicKeyEncryptedData) esk; if (pkesk.getKeyID() != 0) { pkesks.add(pkesk); diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/automaton/InputAlphabet.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/InputAlphabet.java similarity index 95% rename from pgpainless-core/src/main/java/org/pgpainless/decryption_verification/automaton/InputAlphabet.java rename to pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/InputAlphabet.java index ad2a8c55..f73ede34 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/automaton/InputAlphabet.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/InputAlphabet.java @@ -2,7 +2,7 @@ // // SPDX-License-Identifier: Apache-2.0 -package org.pgpainless.decryption_verification.automaton; +package org.pgpainless.decryption_verification.syntax_check; import org.bouncycastle.openpgp.PGPCompressedData; import org.bouncycastle.openpgp.PGPEncryptedDataList; 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/syntax_check/PDA.java similarity index 96% rename from pgpainless-core/src/main/java/org/pgpainless/decryption_verification/automaton/PDA.java rename to pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/PDA.java index 3df3c5b5..0d6ba28c 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/automaton/PDA.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/PDA.java @@ -2,7 +2,7 @@ // // SPDX-License-Identifier: Apache-2.0 -package org.pgpainless.decryption_verification.automaton; +package org.pgpainless.decryption_verification.syntax_check; import org.pgpainless.exception.MalformedOpenPgpMessageException; import org.slf4j.Logger; @@ -10,9 +10,9 @@ import org.slf4j.LoggerFactory; import java.util.Stack; -import static org.pgpainless.decryption_verification.automaton.StackAlphabet.msg; -import static org.pgpainless.decryption_verification.automaton.StackAlphabet.ops; -import static org.pgpainless.decryption_verification.automaton.StackAlphabet.terminus; +import static org.pgpainless.decryption_verification.syntax_check.StackAlphabet.msg; +import static org.pgpainless.decryption_verification.syntax_check.StackAlphabet.ops; +import static org.pgpainless.decryption_verification.syntax_check.StackAlphabet.terminus; public class PDA { diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/automaton/StackAlphabet.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/StackAlphabet.java similarity index 89% rename from pgpainless-core/src/main/java/org/pgpainless/decryption_verification/automaton/StackAlphabet.java rename to pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/StackAlphabet.java index 09865f31..6030fbc8 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/automaton/StackAlphabet.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/StackAlphabet.java @@ -2,7 +2,7 @@ // // SPDX-License-Identifier: Apache-2.0 -package org.pgpainless.decryption_verification.automaton; +package org.pgpainless.decryption_verification.syntax_check; public enum StackAlphabet { /** diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/automaton/package-info.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/package-info.java similarity index 78% rename from pgpainless-core/src/main/java/org/pgpainless/decryption_verification/automaton/package-info.java rename to pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/package-info.java index 80a79e85..4df6af5a 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/automaton/package-info.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/package-info.java @@ -5,4 +5,4 @@ /** * Pushdown Automaton to verify validity of packet sequences according to the OpenPGP Message format. */ -package org.pgpainless.decryption_verification.automaton; +package org.pgpainless.decryption_verification.syntax_check; diff --git a/pgpainless-core/src/main/java/org/pgpainless/exception/MalformedOpenPgpMessageException.java b/pgpainless-core/src/main/java/org/pgpainless/exception/MalformedOpenPgpMessageException.java index 0069209c..cbe78c05 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/exception/MalformedOpenPgpMessageException.java +++ b/pgpainless-core/src/main/java/org/pgpainless/exception/MalformedOpenPgpMessageException.java @@ -4,9 +4,9 @@ package org.pgpainless.exception; -import org.pgpainless.decryption_verification.automaton.InputAlphabet; -import org.pgpainless.decryption_verification.automaton.PDA; -import org.pgpainless.decryption_verification.automaton.StackAlphabet; +import org.pgpainless.decryption_verification.syntax_check.InputAlphabet; +import org.pgpainless.decryption_verification.syntax_check.PDA; +import org.pgpainless.decryption_verification.syntax_check.StackAlphabet; /** * Exception that gets thrown if the OpenPGP message is malformed. diff --git a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/automaton/PDATest.java b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/syntax_check/PDATest.java similarity index 97% rename from pgpainless-core/src/test/java/org/pgpainless/decryption_verification/automaton/PDATest.java rename to pgpainless-core/src/test/java/org/pgpainless/decryption_verification/syntax_check/PDATest.java index 6dbb2cd6..346e1f3b 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/automaton/PDATest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/syntax_check/PDATest.java @@ -1,4 +1,4 @@ -package org.pgpainless.decryption_verification.automaton; +package org.pgpainless.decryption_verification.syntax_check; import org.junit.jupiter.api.Test; import org.pgpainless.exception.MalformedOpenPgpMessageException; From bb1b154745c2e4adb47aaf958e69b82c62052c66 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Fri, 21 Oct 2022 00:40:40 +0200 Subject: [PATCH 45/80] Add more direct PDA tests --- .../syntax_check/PDATest.java | 86 ++++++++++++++----- 1 file changed, 63 insertions(+), 23 deletions(-) diff --git a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/syntax_check/PDATest.java b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/syntax_check/PDATest.java index 346e1f3b..e4877d94 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/syntax_check/PDATest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/syntax_check/PDATest.java @@ -1,10 +1,11 @@ package org.pgpainless.decryption_verification.syntax_check; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + import org.junit.jupiter.api.Test; import org.pgpainless.exception.MalformedOpenPgpMessageException; -import static org.junit.jupiter.api.Assertions.assertTrue; - public class PDATest { @@ -15,11 +16,11 @@ public class PDATest { */ @Test public void testSimpleLiteralMessageIsValid() throws MalformedOpenPgpMessageException { - PDA automaton = new PDA(); - automaton.next(InputAlphabet.LiteralData); - automaton.next(InputAlphabet.EndOfSequence); + PDA check = new PDA(); + check.next(InputAlphabet.LiteralData); + check.next(InputAlphabet.EndOfSequence); - assertTrue(automaton.isValid()); + assertTrue(check.isValid()); } /** @@ -29,13 +30,13 @@ public class PDATest { */ @Test public void testSimpleOpsSignedMesssageIsValid() throws MalformedOpenPgpMessageException { - PDA automaton = new PDA(); - automaton.next(InputAlphabet.OnePassSignature); - automaton.next(InputAlphabet.LiteralData); - automaton.next(InputAlphabet.Signature); - automaton.next(InputAlphabet.EndOfSequence); + PDA check = new PDA(); + check.next(InputAlphabet.OnePassSignature); + check.next(InputAlphabet.LiteralData); + check.next(InputAlphabet.Signature); + check.next(InputAlphabet.EndOfSequence); - assertTrue(automaton.isValid()); + assertTrue(check.isValid()); } @@ -46,12 +47,12 @@ public class PDATest { */ @Test public void testSimplePrependSignedMessageIsValid() throws MalformedOpenPgpMessageException { - PDA automaton = new PDA(); - automaton.next(InputAlphabet.Signature); - automaton.next(InputAlphabet.LiteralData); - automaton.next(InputAlphabet.EndOfSequence); + PDA check = new PDA(); + check.next(InputAlphabet.Signature); + check.next(InputAlphabet.LiteralData); + check.next(InputAlphabet.EndOfSequence); - assertTrue(automaton.isValid()); + assertTrue(check.isValid()); } @@ -62,14 +63,53 @@ public class PDATest { */ @Test public void testOPSSignedCompressedMessageIsValid() throws MalformedOpenPgpMessageException { - PDA automaton = new PDA(); - automaton.next(InputAlphabet.OnePassSignature); - automaton.next(InputAlphabet.CompressedData); + PDA check = new PDA(); + check.next(InputAlphabet.OnePassSignature); + check.next(InputAlphabet.CompressedData); // Here would be a nested PDA for the LiteralData packet - automaton.next(InputAlphabet.Signature); - automaton.next(InputAlphabet.EndOfSequence); + check.next(InputAlphabet.Signature); + check.next(InputAlphabet.EndOfSequence); - assertTrue(automaton.isValid()); + assertTrue(check.isValid()); } + @Test + public void testTwoLiteralDataIsNotValid() { + PDA check = new PDA(); + check.next(InputAlphabet.LiteralData); + assertThrows(MalformedOpenPgpMessageException.class, + () -> check.next(InputAlphabet.LiteralData)); + } + + @Test + public void testTrailingSigIsNotValid() { + PDA check = new PDA(); + check.next(InputAlphabet.LiteralData); + assertThrows(MalformedOpenPgpMessageException.class, + () -> check.next(InputAlphabet.Signature)); + } + + @Test + public void testOPSWithPrependedSigIsValid() { + PDA check = new PDA(); + check.next(InputAlphabet.Signature); + check.next(InputAlphabet.OnePassSignature); + check.next(InputAlphabet.LiteralData); + check.next(InputAlphabet.Signature); + check.next(InputAlphabet.EndOfSequence); + + assertTrue(check.isValid()); + } + + @Test + public void testPrependedSigInsideOPSSignedMessageIsValid() { + PDA check = new PDA(); + check.next(InputAlphabet.OnePassSignature); + check.next(InputAlphabet.Signature); + check.next(InputAlphabet.LiteralData); + check.next(InputAlphabet.Signature); + check.next(InputAlphabet.EndOfSequence); + + assertTrue(check.isValid()); + } } From 7aaeb8ccfd9b2bfc4e2b695d679aea938e586f60 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Fri, 21 Oct 2022 13:46:33 +0200 Subject: [PATCH 46/80] Further increase coverage of PDA class --- .../syntax_check/PDATest.java | 78 +++++++++++++++++++ 1 file changed, 78 insertions(+) diff --git a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/syntax_check/PDATest.java b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/syntax_check/PDATest.java index e4877d94..8fa107f6 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/syntax_check/PDATest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/syntax_check/PDATest.java @@ -73,6 +73,42 @@ public class PDATest { assertTrue(check.isValid()); } + @Test + public void testOPSSignedEncryptedMessageIsValid() { + PDA check = new PDA(); + check.next(InputAlphabet.OnePassSignature); + check.next(InputAlphabet.EncryptedData); + check.next(InputAlphabet.Signature); + check.next(InputAlphabet.EndOfSequence); + assertTrue(check.isValid()); + } + + @Test + public void anyInputAfterEOSIsNotValid() { + PDA check = new PDA(); + check.next(InputAlphabet.LiteralData); + check.next(InputAlphabet.EndOfSequence); + assertThrows(MalformedOpenPgpMessageException.class, + () -> check.next(InputAlphabet.Signature)); + } + + @Test + public void testEncryptedMessageWithAppendedStandalongSigIsNotValid() { + PDA check = new PDA(); + check.next(InputAlphabet.EncryptedData); + assertThrows(MalformedOpenPgpMessageException.class, + () -> check.next(InputAlphabet.Signature)); + } + + @Test + public void testOPSSignedEncryptedMessageWithMissingSigIsNotValid() { + PDA check = new PDA(); + check.next(InputAlphabet.OnePassSignature); + check.next(InputAlphabet.EncryptedData); + assertThrows(MalformedOpenPgpMessageException.class, + () -> check.next(InputAlphabet.EndOfSequence)); + } + @Test public void testTwoLiteralDataIsNotValid() { PDA check = new PDA(); @@ -89,6 +125,48 @@ public class PDATest { () -> check.next(InputAlphabet.Signature)); } + @Test + public void testOPSAloneIsNotValid() { + PDA check = new PDA(); + check.next(InputAlphabet.OnePassSignature); + assertThrows(MalformedOpenPgpMessageException.class, + () -> check.next(InputAlphabet.EndOfSequence)); + } + + @Test + public void testOPSLitWithMissingSigIsNotValid() { + PDA check = new PDA(); + check.next(InputAlphabet.OnePassSignature); + check.next(InputAlphabet.LiteralData); + assertThrows(MalformedOpenPgpMessageException.class, + () -> check.next(InputAlphabet.EndOfSequence)); + } + + @Test + public void testCompressedMessageWithStandalongAppendedSigIsNotValid() { + PDA check = new PDA(); + check.next(InputAlphabet.CompressedData); + assertThrows(MalformedOpenPgpMessageException.class, + () -> check.next(InputAlphabet.Signature)); + } + + @Test + public void testOPSCompressedDataWithMissingSigIsNotValid() { + PDA check = new PDA(); + check.next(InputAlphabet.OnePassSignature); + check.next(InputAlphabet.CompressedData); + assertThrows(MalformedOpenPgpMessageException.class, + () -> check.next(InputAlphabet.EndOfSequence)); + } + + @Test + public void testCompressedMessageFollowedByTrailingLiteralDataIsNotValid() { + PDA check = new PDA(); + check.next(InputAlphabet.CompressedData); + assertThrows(MalformedOpenPgpMessageException.class, + () -> check.next(InputAlphabet.LiteralData)); + } + @Test public void testOPSWithPrependedSigIsValid() { PDA check = new PDA(); From 09d036f17bdaccb84d928d5b2c7b5102ef932534 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Fri, 21 Oct 2022 16:16:45 +0200 Subject: [PATCH 47/80] Fix CRCing test and fully depend on new stream for decryption --- .../DecryptionBuilder.java | 2 +- .../MessageMetadata.java | 22 ++- .../OpenPgpMessageInputStream.java | 152 +++++++++++++----- .../TeeBCPGInputStream.java | 14 +- .../OpenPgpMessageInputStreamTest.java | 2 +- 5 files changed, 137 insertions(+), 55 deletions(-) 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 68a68847..0baf4124 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 @@ -31,7 +31,7 @@ public class DecryptionBuilder implements DecryptionBuilderInterface { throw new IllegalArgumentException("Consumer options cannot be null."); } - return DecryptionStreamFactory.create(inputStream, consumerOptions); + return OpenPgpMessageInputStream.create(inputStream, consumerOptions); } } } 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 index 59f8052a..7811578e 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/MessageMetadata.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/MessageMetadata.java @@ -164,19 +164,35 @@ public class MessageMetadata { } public String getFilename() { - return findLiteralData().getFileName(); + LiteralData literalData = findLiteralData(); + if (literalData == null) { + return null; + } + return literalData.getFileName(); } public Date getModificationDate() { - return findLiteralData().getModificationDate(); + LiteralData literalData = findLiteralData(); + if (literalData == null) { + return null; + } + return literalData.getModificationDate(); } public StreamEncoding getFormat() { - return findLiteralData().getFormat(); + LiteralData literalData = findLiteralData(); + if (literalData == null) { + return null; + } + return literalData.getFormat(); } private LiteralData findLiteralData() { Nested nested = message.child; + if (nested == null) { + return null; + } + while (nested.hasNestedChild()) { Layer layer = (Layer) nested; nested = layer.child; 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 a2fa2351..dee59058 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 @@ -37,6 +37,8 @@ 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.util.io.Streams; +import org.bouncycastle.util.io.TeeInputStream; import org.pgpainless.PGPainless; import org.pgpainless.algorithm.CompressionAlgorithm; import org.pgpainless.algorithm.EncryptionPurpose; @@ -79,32 +81,87 @@ public class OpenPgpMessageInputStream extends DecryptionStream { // Options to consume the data protected final ConsumerOptions options; + + private final Policy policy; // Pushdown Automaton to verify validity of OpenPGP packet sequence in an OpenPGP message protected final PDA syntaxVerifier = new PDA(); // InputStream of OpenPGP packets protected TeeBCPGInputStream packetInputStream; - // InputStream of a nested data packet + // InputStream of a data packet containing nested data protected InputStream nestedInputStream; private boolean closed = false; private final Signatures signatures; private final MessageMetadata.Layer metadata; - private final Policy policy; - public OpenPgpMessageInputStream(@Nonnull InputStream inputStream, + /** + * Create an {@link OpenPgpMessageInputStream} suitable for decryption and verification of + * OpenPGP messages and signatures. + * This constructor will use the global PGPainless {@link Policy}. + * + * @param inputStream underlying input stream + * @param options options for consuming the stream + * + * @throws IOException in case of an IO error + * @throws PGPException in case of an OpenPGP error + */ + public static OpenPgpMessageInputStream create(@Nonnull InputStream inputStream, @Nonnull ConsumerOptions options) throws IOException, PGPException { - this(inputStream, options, PGPainless.getPolicy()); + return create(inputStream, options, PGPainless.getPolicy()); } - public OpenPgpMessageInputStream(@Nonnull InputStream inputStream, + /** + * Create an {@link OpenPgpMessageInputStream} suitable for decryption and verification of + * OpenPGP messages and signatures. + * + * @param inputStream underlying input stream containing the OpenPGP message + * @param options options for consuming the message + * @param policy policy for acceptable algorithms etc. + * + * @throws PGPException in case of an OpenPGP error + * @throws IOException in case of an IO error + */ + public static OpenPgpMessageInputStream create(@Nonnull InputStream inputStream, @Nonnull ConsumerOptions options, @Nonnull Policy policy) throws PGPException, IOException { - this( - prepareInputStream(inputStream, options, policy), - options, new MessageMetadata.Message(), policy); + return create(inputStream, options, new MessageMetadata.Message(), policy); + } + + protected static OpenPgpMessageInputStream create(@Nonnull InputStream inputStream, + @Nonnull ConsumerOptions options, + @Nonnull MessageMetadata.Layer metadata, + @Nonnull Policy policy) + throws IOException, PGPException { + OpenPgpInputStream openPgpIn = new OpenPgpInputStream(inputStream); + openPgpIn.reset(); + + if (openPgpIn.isNonOpenPgp() || options.isForceNonOpenPgpData()) { + return new OpenPgpMessageInputStream(Type.non_openpgp, + openPgpIn, options, metadata, policy); + } + + if (openPgpIn.isBinaryOpenPgp()) { + // Simply consume OpenPGP message + return new OpenPgpMessageInputStream(Type.standard, + openPgpIn, options, metadata, policy); + } + + if (openPgpIn.isAsciiArmored()) { + ArmoredInputStream armorIn = ArmoredInputStreamFactory.get(openPgpIn); + if (armorIn.isClearText()) { + return new OpenPgpMessageInputStream(Type.cleartext_signed, + armorIn, options, metadata, policy); + } else { + // Simply consume dearmored OpenPGP message + return new OpenPgpMessageInputStream(Type.standard, + armorIn, options, metadata, policy); + } + } else { + throw new AssertionError("Huh?"); + } } protected OpenPgpMessageInputStream(@Nonnull InputStream inputStream, @@ -131,52 +188,56 @@ public class OpenPgpMessageInputStream extends DecryptionStream { consumePackets(); } - protected OpenPgpMessageInputStream(@Nonnull InputStream inputStream, - @Nonnull Policy policy, - @Nonnull ConsumerOptions options) { + enum Type { + standard, + cleartext_signed, + non_openpgp + } + + protected OpenPgpMessageInputStream(@Nonnull Type type, + @Nonnull InputStream inputStream, + @Nonnull ConsumerOptions options, + @Nonnull MessageMetadata.Layer metadata, + @Nonnull Policy policy) throws PGPException, IOException { super(OpenPgpMetadata.getBuilder()); this.policy = policy; this.options = options; - this.metadata = new MessageMetadata.Message(); + this.metadata = metadata; this.signatures = new Signatures(options); - this.signatures.addDetachedSignatures(options.getDetachedSignatures()); - this.packetInputStream = new TeeBCPGInputStream(BCPGInputStream.wrap(inputStream), signatures); - } - private static InputStream prepareInputStream(InputStream inputStream, ConsumerOptions options, Policy policy) - throws IOException, PGPException { - OpenPgpInputStream openPgpIn = new OpenPgpInputStream(inputStream); - openPgpIn.reset(); - - if (openPgpIn.isBinaryOpenPgp()) { - return openPgpIn; + if (metadata instanceof MessageMetadata.Message) { + this.signatures.addDetachedSignatures(options.getDetachedSignatures()); } - if (openPgpIn.isAsciiArmored()) { - ArmoredInputStream armorIn = ArmoredInputStreamFactory.get(openPgpIn); - if (armorIn.isClearText()) { - return parseCleartextSignedMessage(armorIn, options, policy); - } else { - return armorIn; - } - } else { - return openPgpIn; + switch (type) { + case standard: + // tee out packet bytes for signature verification + packetInputStream = new TeeBCPGInputStream(BCPGInputStream.wrap(inputStream), this.signatures); + + // *omnomnom* + consumePackets(); + break; + case cleartext_signed: + MultiPassStrategy multiPassStrategy = options.getMultiPassStrategy(); + PGPSignatureList detachedSignatures = ClearsignedMessageUtil + .detachSignaturesFromInbandClearsignedMessage( + inputStream, multiPassStrategy.getMessageOutputStream()); + + for (PGPSignature signature : detachedSignatures) { + options.addVerificationOfDetachedSignature(signature); + } + + options.forceNonOpenPgpData(); + packetInputStream = null; + nestedInputStream = new TeeInputStream(multiPassStrategy.getMessageInputStream(), this.signatures); + break; + case non_openpgp: + packetInputStream = null; + nestedInputStream = new TeeInputStream(inputStream, this.signatures); + break; } } - private static DecryptionStream parseCleartextSignedMessage(ArmoredInputStream armorIn, ConsumerOptions options, Policy policy) - throws IOException, PGPException { - MultiPassStrategy multiPassStrategy = options.getMultiPassStrategy(); - PGPSignatureList signatures = ClearsignedMessageUtil.detachSignaturesFromInbandClearsignedMessage(armorIn, multiPassStrategy.getMessageOutputStream()); - - for (PGPSignature signature : signatures) { - options.addVerificationOfDetachedSignature(signature); - } - - options.forceNonOpenPgpData(); - return new OpenPgpMessageInputStream(multiPassStrategy.getMessageInputStream(), policy, options); - } - /** * Consume OpenPGP packets from the current {@link BCPGInputStream}. * Once an OpenPGP packet with nested data (Literal Data, Compressed Data, Encrypted Data) is reached, @@ -196,6 +257,9 @@ public class OpenPgpMessageInputStream extends DecryptionStream { private void consumePackets() throws IOException, PGPException { OpenPgpPacket nextPacket; + if (packetInputStream == null) { + return; + } loop: // we break this when we go deeper. while ((nextPacket = packetInputStream.nextPacketTag()) != null) { diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/TeeBCPGInputStream.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/TeeBCPGInputStream.java index 2efcfc43..1ca1b3f9 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/TeeBCPGInputStream.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/TeeBCPGInputStream.java @@ -4,6 +4,11 @@ package org.pgpainless.decryption_verification; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.NoSuchElementException; + import org.bouncycastle.bcpg.BCPGInputStream; import org.bouncycastle.bcpg.MarkerPacket; import org.bouncycastle.bcpg.Packet; @@ -15,11 +20,6 @@ import org.bouncycastle.openpgp.PGPOnePassSignature; import org.bouncycastle.openpgp.PGPSignature; import org.pgpainless.algorithm.OpenPgpPacket; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.util.NoSuchElementException; - /** * Since we need to update signatures with data from the underlying stream, this class is used to tee out the data. * Unfortunately we cannot simply override {@link BCPGInputStream#read()} to tee the data out though, since @@ -96,7 +96,6 @@ public class TeeBCPGInputStream { return markerPacket; } - public void close() throws IOException { this.packetInputStream.close(); this.delayedTee.close(); @@ -122,6 +121,9 @@ public class TeeBCPGInputStream { last = inputStream.read(); return last; } catch (IOException e) { + if ("crc check failed in armored message.".equals(e.getMessage())) { + throw e; + } return -1; } } 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 a8969047..f9539149 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 @@ -677,7 +677,7 @@ public class OpenPgpMessageInputStreamTest { throws IOException, PGPException { ByteArrayInputStream bytesIn = new ByteArrayInputStream(armored.getBytes(StandardCharsets.UTF_8)); ArmoredInputStream armorIn = ArmoredInputStreamFactory.get(bytesIn); - OpenPgpMessageInputStream pgpIn = new OpenPgpMessageInputStream(armorIn, options); + OpenPgpMessageInputStream pgpIn = OpenPgpMessageInputStream.create(armorIn, options); return pgpIn; } } From 9ec4c47309698dbb67c9f5fb0b611ff75ec978b7 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Fri, 21 Oct 2022 16:17:30 +0200 Subject: [PATCH 48/80] Delete old DecryptionStreamFactory --- .../DecryptionStreamFactory.java | 692 ------------------ 1 file changed, 692 deletions(-) delete mode 100644 pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionStreamFactory.java 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 deleted file mode 100644 index 451750ee..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionStreamFactory.java +++ /dev/null @@ -1,692 +0,0 @@ -// SPDX-FileCopyrightText: 2018 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.decryption_verification; - -import java.io.IOException; -import java.io.InputStream; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Iterator; -import java.util.List; -import java.util.Map; -import java.util.NoSuchElementException; -import java.util.Set; -import javax.annotation.Nonnull; -import javax.annotation.Nullable; - -import org.bouncycastle.bcpg.ArmoredInputStream; -import org.bouncycastle.openpgp.PGPCompressedData; -import org.bouncycastle.openpgp.PGPEncryptedData; -import org.bouncycastle.openpgp.PGPEncryptedDataList; -import org.bouncycastle.openpgp.PGPException; -import org.bouncycastle.openpgp.PGPLiteralData; -import org.bouncycastle.openpgp.PGPObjectFactory; -import org.bouncycastle.openpgp.PGPOnePassSignature; -import org.bouncycastle.openpgp.PGPOnePassSignatureList; -import org.bouncycastle.openpgp.PGPPBEEncryptedData; -import org.bouncycastle.openpgp.PGPPrivateKey; -import org.bouncycastle.openpgp.PGPPublicKey; -import org.bouncycastle.openpgp.PGPPublicKeyEncryptedData; -import org.bouncycastle.openpgp.PGPPublicKeyRing; -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.operator.PBEDataDecryptorFactory; -import org.bouncycastle.openpgp.operator.PGPContentVerifierBuilderProvider; -import org.bouncycastle.openpgp.operator.PublicKeyDataDecryptorFactory; -import org.bouncycastle.openpgp.operator.SessionKeyDataDecryptorFactory; -import org.pgpainless.PGPainless; -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; -import org.pgpainless.exception.MissingLiteralDataException; -import org.pgpainless.exception.MissingPassphraseException; -import org.pgpainless.exception.SignatureValidationException; -import org.pgpainless.exception.UnacceptableAlgorithmException; -import org.pgpainless.implementation.ImplementationFactory; -import org.pgpainless.key.SubkeyIdentifier; -import org.pgpainless.key.info.KeyRingInfo; -import org.pgpainless.key.protection.SecretKeyRingProtector; -import org.pgpainless.key.protection.UnlockSecretKey; -import org.pgpainless.signature.SignatureUtils; -import org.pgpainless.signature.consumer.SignatureCheck; -import org.pgpainless.signature.consumer.OnePassSignatureCheck; -import org.pgpainless.util.ArmoredInputStreamFactory; -import org.pgpainless.util.Passphrase; -import org.pgpainless.util.SessionKey; -import org.pgpainless.util.Tuple; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -public final class DecryptionStreamFactory { - - - private static final Logger LOGGER = LoggerFactory.getLogger(DecryptionStreamFactory.class); - // Maximum nesting depth of packets (e.g. compression, encryption...) - private static final int MAX_PACKET_NESTING_DEPTH = 16; - - private final ConsumerOptions options; - private final OpenPgpMetadata.Builder resultBuilder = OpenPgpMetadata.getBuilder(); - private final List onePassSignatureChecks = new ArrayList<>(); - private final List signatureChecks = new ArrayList<>(); - private final Map onePassSignaturesWithMissingCert = new HashMap<>(); - - private static final PGPContentVerifierBuilderProvider verifierBuilderProvider = - ImplementationFactory.getInstance().getPGPContentVerifierBuilderProvider(); - private IntegrityProtectedInputStream integrityProtectedEncryptedInputStream; - - - public static DecryptionStream create(@Nonnull InputStream inputStream, - @Nonnull ConsumerOptions options) - throws PGPException, IOException { - OpenPgpInputStream openPgpInputStream = new OpenPgpInputStream(inputStream); - openPgpInputStream.reset(); - if (openPgpInputStream.isBinaryOpenPgp()) { - return new OpenPgpMessageInputStream(openPgpInputStream, options); - } else if (openPgpInputStream.isAsciiArmored()) { - ArmoredInputStream armorIn = ArmoredInputStreamFactory.get(openPgpInputStream); - if (armorIn.isClearText()) { - return createOld(openPgpInputStream, options); - } else { - return new OpenPgpMessageInputStream(armorIn, options); - } - } else if (openPgpInputStream.isNonOpenPgp()) { - return createOld(openPgpInputStream, options); - } else { - throw new IOException("What?"); - } - } - - public static DecryptionStream createOld(@Nonnull InputStream inputStream, - @Nonnull ConsumerOptions options) throws IOException, PGPException { - DecryptionStreamFactory factory = new DecryptionStreamFactory(options); - OpenPgpInputStream openPgpIn = new OpenPgpInputStream(inputStream); - return factory.parseOpenPGPDataAndCreateDecryptionStream(openPgpIn); - } - - public DecryptionStreamFactory(ConsumerOptions options) { - this.options = options; - initializeDetachedSignatures(options.getDetachedSignatures()); - } - - private void initializeDetachedSignatures(Set signatures) { - for (PGPSignature signature : signatures) { - long issuerKeyId = SignatureUtils.determineIssuerKeyId(signature); - PGPPublicKeyRing signingKeyRing = findSignatureVerificationKeyRing(issuerKeyId); - if (signingKeyRing == null) { - SignatureValidationException ex = new SignatureValidationException( - "Missing verification certificate " + Long.toHexString(issuerKeyId)); - resultBuilder.addInvalidDetachedSignature(new SignatureVerification(signature, null), ex); - continue; - } - PGPPublicKey signingKey = signingKeyRing.getPublicKey(issuerKeyId); - SubkeyIdentifier signingKeyIdentifier = new SubkeyIdentifier(signingKeyRing, signingKey.getKeyID()); - try { - signature.init(verifierBuilderProvider, signingKey); - SignatureCheck detachedSignature = - new SignatureCheck(signature, signingKeyRing, signingKeyIdentifier); - signatureChecks.add(detachedSignature); - } catch (PGPException e) { - SignatureValidationException ex = new SignatureValidationException( - "Cannot verify detached signature made by " + signingKeyIdentifier + ".", e); - resultBuilder.addInvalidDetachedSignature(new SignatureVerification(signature, signingKeyIdentifier), ex); - } - } - } - - private DecryptionStream parseOpenPGPDataAndCreateDecryptionStream(OpenPgpInputStream openPgpIn) - throws IOException, PGPException { - - InputStream pgpInStream; - InputStream outerDecodingStream; - PGPObjectFactory objectFactory; - - // Non-OpenPGP data. We are probably verifying detached signatures - if (openPgpIn.isNonOpenPgp() || options.isForceNonOpenPgpData()) { - outerDecodingStream = openPgpIn; - pgpInStream = wrapInVerifySignatureStream(outerDecodingStream, null); - return new DecryptionStreamImpl(pgpInStream, resultBuilder, integrityProtectedEncryptedInputStream, null); - } - - // Data appears to be OpenPGP message, - // or we handle it as such, since user provided a session-key for decryption - if (openPgpIn.isLikelyOpenPgpMessage() || - (openPgpIn.isBinaryOpenPgp() && options.getSessionKey() != null)) { - outerDecodingStream = openPgpIn; - objectFactory = ImplementationFactory.getInstance().getPGPObjectFactory(outerDecodingStream); - // Parse OpenPGP message - pgpInStream = processPGPPackets(objectFactory, 1); - return new DecryptionStreamImpl(pgpInStream, - resultBuilder, integrityProtectedEncryptedInputStream, null); - } - - if (openPgpIn.isAsciiArmored()) { - ArmoredInputStream armoredInputStream = ArmoredInputStreamFactory.get(openPgpIn); - if (armoredInputStream.isClearText()) { - resultBuilder.setCleartextSigned(); - return parseCleartextSignedMessage(armoredInputStream); - } else { - outerDecodingStream = armoredInputStream; - objectFactory = ImplementationFactory.getInstance().getPGPObjectFactory(outerDecodingStream); - // Parse OpenPGP message - pgpInStream = processPGPPackets(objectFactory, 1); - return new DecryptionStreamImpl(pgpInStream, - resultBuilder, integrityProtectedEncryptedInputStream, - outerDecodingStream); - } - } - - throw new PGPException("Not sure how to handle the input stream."); - } - - private DecryptionStreamImpl parseCleartextSignedMessage(ArmoredInputStream armorIn) - throws IOException, PGPException { - resultBuilder.setCompressionAlgorithm(CompressionAlgorithm.UNCOMPRESSED) - .setFileEncoding(StreamEncoding.TEXT); - - 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 DecryptionStreamImpl(verifyIn, resultBuilder, integrityProtectedEncryptedInputStream, - null); - } - - private InputStream wrapInVerifySignatureStream(InputStream bufferedIn, @Nullable PGPObjectFactory objectFactory) { - return new SignatureInputStream.VerifySignatures( - bufferedIn, objectFactory, onePassSignatureChecks, - onePassSignaturesWithMissingCert, signatureChecks, options, - resultBuilder); - } - - private InputStream processPGPPackets(@Nonnull PGPObjectFactory objectFactory, int depth) - throws IOException, PGPException { - if (depth >= MAX_PACKET_NESTING_DEPTH) { - throw new PGPException("Maximum depth of nested packages exceeded."); - } - Object nextPgpObject; - 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); - } - } - } catch (FinalIOException e) { - throw e; - } catch (IOException e) { - if (depth == 1 && e.getMessage().contains("invalid armor")) { - throw e; - } - if (depth == 1 && e.getMessage().contains("unknown object in stream:")) { - throw new MissingLiteralDataException("No Literal Data Packet found."); - } else { - throw new FinalIOException(e); - } - } - - throw new MissingLiteralDataException("No Literal Data Packet found"); - } - - private InputStream processPGPEncryptedDataList(PGPEncryptedDataList pgpEncryptedDataList, int depth) - throws PGPException, IOException { - LOGGER.debug("Depth {}: Encountered PGPEncryptedDataList", depth); - - SessionKey sessionKey = options.getSessionKey(); - if (sessionKey != null) { - integrityProtectedEncryptedInputStream = decryptWithProvidedSessionKey(pgpEncryptedDataList, sessionKey); - PGPObjectFactory factory = ImplementationFactory.getInstance().getPGPObjectFactory(integrityProtectedEncryptedInputStream); - return processPGPPackets(factory, ++depth); - } - - InputStream decryptedDataStream = decryptSessionKey(pgpEncryptedDataList); - PGPObjectFactory factory = ImplementationFactory.getInstance().getPGPObjectFactory(decryptedDataStream); - return processPGPPackets(factory, ++depth); - } - - private IntegrityProtectedInputStream decryptWithProvidedSessionKey( - PGPEncryptedDataList pgpEncryptedDataList, - SessionKey sessionKey) - throws PGPException { - SessionKeyDataDecryptorFactory decryptorFactory = ImplementationFactory.getInstance() - .getSessionKeyDataDecryptorFactory(sessionKey); - InputStream decryptedDataStream = null; - PGPEncryptedData encryptedData = null; - for (PGPEncryptedData pgpEncryptedData : pgpEncryptedDataList) { - encryptedData = pgpEncryptedData; - if (!options.isIgnoreMDCErrors() && !encryptedData.isIntegrityProtected()) { - throw new MessageNotIntegrityProtectedException(); - } - - if (encryptedData instanceof PGPPBEEncryptedData) { - PGPPBEEncryptedData pbeEncrypted = (PGPPBEEncryptedData) encryptedData; - decryptedDataStream = pbeEncrypted.getDataStream(decryptorFactory); - break; - } else if (encryptedData instanceof PGPPublicKeyEncryptedData) { - PGPPublicKeyEncryptedData pkEncrypted = (PGPPublicKeyEncryptedData) encryptedData; - decryptedDataStream = pkEncrypted.getDataStream(decryptorFactory); - break; - } - } - - if (decryptedDataStream == null) { - throw new PGPException("No valid PGP data encountered."); - } - - resultBuilder.setSessionKey(sessionKey); - throwIfAlgorithmIsRejected(sessionKey.getAlgorithm()); - integrityProtectedEncryptedInputStream = - new IntegrityProtectedInputStream(decryptedDataStream, encryptedData, options); - return integrityProtectedEncryptedInputStream; - } - - private InputStream processPGPCompressedData(PGPCompressedData pgpCompressedData, int depth) - throws PGPException, IOException { - try { - CompressionAlgorithm compressionAlgorithm = CompressionAlgorithm.requireFromId(pgpCompressedData.getAlgorithm()); - LOGGER.debug("Depth {}: Encountered PGPCompressedData: {}", depth, compressionAlgorithm); - resultBuilder.setCompressionAlgorithm(compressionAlgorithm); - } catch (NoSuchElementException e) { - throw new PGPException("Unknown compression algorithm encountered.", e); - } - - InputStream inflatedDataStream = pgpCompressedData.getDataStream(); - PGPObjectFactory objectFactory = ImplementationFactory.getInstance().getPGPObjectFactory(inflatedDataStream); - - return processPGPPackets(objectFactory, ++depth); - } - - private InputStream processOnePassSignatureList( - @Nonnull PGPObjectFactory objectFactory, - PGPOnePassSignatureList onePassSignatures, - int depth) - throws PGPException, IOException { - LOGGER.debug("Depth {}: Encountered PGPOnePassSignatureList of size {}", depth, onePassSignatures.size()); - initOnePassSignatures(onePassSignatures); - return processPGPPackets(objectFactory, depth); - } - - private InputStream processPGPLiteralData( - @Nonnull PGPObjectFactory objectFactory, - PGPLiteralData pgpLiteralData, - int depth) { - LOGGER.debug("Depth {}: Found PGPLiteralData", depth); - InputStream literalDataInputStream = pgpLiteralData.getInputStream(); - - resultBuilder.setFileName(pgpLiteralData.getFileName()) - .setModificationDate(pgpLiteralData.getModificationTime()) - .setFileEncoding(StreamEncoding.requireFromCode(pgpLiteralData.getFormat())); - - if (onePassSignatureChecks.isEmpty() && onePassSignaturesWithMissingCert.isEmpty()) { - LOGGER.debug("No OnePassSignatures found -> We are done"); - return literalDataInputStream; - } - - return new SignatureInputStream.VerifySignatures(literalDataInputStream, objectFactory, - onePassSignatureChecks, onePassSignaturesWithMissingCert, signatureChecks, options, resultBuilder) { - }; - } - - private InputStream decryptSessionKey(@Nonnull PGPEncryptedDataList encryptedDataList) - throws PGPException { - Iterator encryptedDataIterator = encryptedDataList.getEncryptedDataObjects(); - if (!encryptedDataIterator.hasNext()) { - throw new PGPException("Decryption failed - EncryptedDataList has no items"); - } - - PGPPrivateKey decryptionKey = null; - PGPPublicKeyEncryptedData encryptedSessionKey = null; - - List passphraseProtected = new ArrayList<>(); - List publicKeyProtected = new ArrayList<>(); - List> postponedDueToMissingPassphrase = new ArrayList<>(); - - // Sort PKESK and SKESK packets - while (encryptedDataIterator.hasNext()) { - PGPEncryptedData encryptedData = encryptedDataIterator.next(); - - if (!encryptedData.isIntegrityProtected() && !options.isIgnoreMDCErrors()) { - throw new MessageNotIntegrityProtectedException(); - } - - // SKESK - if (encryptedData instanceof PGPPBEEncryptedData) { - passphraseProtected.add((PGPPBEEncryptedData) encryptedData); - } - // PKESK - else if (encryptedData instanceof PGPPublicKeyEncryptedData) { - publicKeyProtected.add((PGPPublicKeyEncryptedData) encryptedData); - } - } - - // Try decryption with passphrases first - for (PGPPBEEncryptedData pbeEncryptedData : passphraseProtected) { - for (Passphrase passphrase : options.getDecryptionPassphrases()) { - PBEDataDecryptorFactory passphraseDecryptor = ImplementationFactory.getInstance() - .getPBEDataDecryptorFactory(passphrase); - try { - InputStream decryptedDataStream = pbeEncryptedData.getDataStream(passphraseDecryptor); - - PGPSessionKey pgpSessionKey = pbeEncryptedData.getSessionKey(passphraseDecryptor); - SessionKey sessionKey = new SessionKey(pgpSessionKey); - resultBuilder.setSessionKey(sessionKey); - - throwIfAlgorithmIsRejected(sessionKey.getAlgorithm()); - - integrityProtectedEncryptedInputStream = - new IntegrityProtectedInputStream(decryptedDataStream, pbeEncryptedData, options); - - return integrityProtectedEncryptedInputStream; - } catch (PGPException e) { - LOGGER.debug("Probable passphrase mismatch, skip PBE encrypted data block", e); - } - } - } - - // Try custom PublicKeyDataDecryptorFactories (e.g. hardware-backed). - Map customFactories = options.getCustomDecryptorFactories(); - for (PGPPublicKeyEncryptedData publicKeyEncryptedData : publicKeyProtected) { - Long keyId = publicKeyEncryptedData.getKeyID(); - if (!customFactories.containsKey(keyId)) { - continue; - } - - PublicKeyDataDecryptorFactory decryptorFactory = customFactories.get(keyId); - try { - InputStream decryptedDataStream = publicKeyEncryptedData.getDataStream(decryptorFactory); - PGPSessionKey pgpSessionKey = publicKeyEncryptedData.getSessionKey(decryptorFactory); - SessionKey sessionKey = new SessionKey(pgpSessionKey); - resultBuilder.setSessionKey(sessionKey); - - throwIfAlgorithmIsRejected(sessionKey.getAlgorithm()); - - integrityProtectedEncryptedInputStream = - new IntegrityProtectedInputStream(decryptedDataStream, publicKeyEncryptedData, options); - - return integrityProtectedEncryptedInputStream; - } catch (PGPException e) { - LOGGER.debug("Decryption with custom PublicKeyDataDecryptorFactory failed", e); - } - } - - // Then try decryption with public key encryption - for (PGPPublicKeyEncryptedData publicKeyEncryptedData : publicKeyProtected) { - PGPPrivateKey privateKey = null; - if (options.getDecryptionKeys().isEmpty()) { - break; - } - - long keyId = publicKeyEncryptedData.getKeyID(); - // Wildcard KeyID - if (keyId == 0L) { - LOGGER.debug("Hidden recipient detected. Try to decrypt with all available secret keys."); - for (PGPSecretKeyRing secretKeys : options.getDecryptionKeys()) { - if (privateKey != null) { - break; - } - KeyRingInfo info = new KeyRingInfo(secretKeys); - List encryptionSubkeys = info.getEncryptionSubkeys(EncryptionPurpose.ANY); - for (PGPPublicKey pubkey : encryptionSubkeys) { - PGPSecretKey secretKey = secretKeys.getSecretKey(pubkey.getKeyID()); - // Skip missing secret key - if (secretKey == null) { - continue; - } - - privateKey = tryPublicKeyDecryption(secretKeys, secretKey, publicKeyEncryptedData, - postponedDueToMissingPassphrase, true); - } - } - } - // Non-wildcard key-id - else { - LOGGER.debug("PGPEncryptedData is encrypted for key {}", Long.toHexString(keyId)); - resultBuilder.addRecipientKeyId(keyId); - - PGPSecretKeyRing secretKeys = findDecryptionKeyRing(keyId); - if (secretKeys == null) { - LOGGER.debug("Missing certificate of {}. Skip.", Long.toHexString(keyId)); - continue; - } - - // Make sure that the recipient key is encryption capable and non-expired - KeyRingInfo info = new KeyRingInfo(secretKeys); - List encryptionSubkeys = info.getEncryptionSubkeys(EncryptionPurpose.ANY); - - PGPSecretKey secretKey = null; - for (PGPPublicKey pubkey : encryptionSubkeys) { - if (pubkey.getKeyID() == keyId) { - secretKey = secretKeys.getSecretKey(keyId); - break; - } - } - - if (secretKey == null) { - LOGGER.debug("Key " + Long.toHexString(keyId) + " is not valid or not capable for decryption."); - } else { - privateKey = tryPublicKeyDecryption(secretKeys, secretKey, publicKeyEncryptedData, - postponedDueToMissingPassphrase, true); - } - } - if (privateKey == null) { - continue; - } - decryptionKey = privateKey; - encryptedSessionKey = publicKeyEncryptedData; - break; - } - - // Try postponed keys with missing passphrases (will cause missing passphrase callbacks to fire) - if (encryptedSessionKey == null) { - - if (options.getMissingKeyPassphraseStrategy() == MissingKeyPassphraseStrategy.THROW_EXCEPTION) { - // Non-interactive mode: Throw an exception with all locked decryption keys - Set keyIds = new HashSet<>(); - for (Tuple k : postponedDueToMissingPassphrase) { - keyIds.add(k.getA()); - } - if (!keyIds.isEmpty()) { - throw new MissingPassphraseException(keyIds); - } - } - else if (options.getMissingKeyPassphraseStrategy() == MissingKeyPassphraseStrategy.INTERACTIVE) { - // Interactive mode: Fire protector callbacks to get passphrases interactively - for (Tuple missingPassphrases : postponedDueToMissingPassphrase) { - SubkeyIdentifier keyId = missingPassphrases.getA(); - PGPPublicKeyEncryptedData publicKeyEncryptedData = missingPassphrases.getB(); - PGPSecretKeyRing secretKeys = findDecryptionKeyRing(keyId.getKeyId()); - PGPSecretKey secretKey = secretKeys.getSecretKey(keyId.getSubkeyId()); - - PGPPrivateKey privateKey = tryPublicKeyDecryption(secretKeys, secretKey, publicKeyEncryptedData, - postponedDueToMissingPassphrase, false); - if (privateKey == null) { - continue; - } - - decryptionKey = privateKey; - encryptedSessionKey = publicKeyEncryptedData; - break; - } - } else { - throw new IllegalStateException("Invalid PostponedKeysStrategy set in consumer options."); - } - - } - - return decryptWith(encryptedSessionKey, decryptionKey); - } - - /** - * Try decryption of the provided public-key-encrypted-data using the given secret key. - * If the secret key is encrypted and the secret key protector does not have a passphrase available and the boolean - * postponeIfMissingPassphrase is true, data decryption is postponed by pushing a tuple of the encrypted data decryption key - * identifier to the postponed list. - * - * This method only returns a non-null private key, if the private key is able to decrypt the message successfully. - * - * @param secretKeys secret key ring - * @param secretKey secret key - * @param publicKeyEncryptedData encrypted data which is tried to decrypt using the secret key - * @param postponed list of postponed decryptions due to missing secret key passphrases - * @param postponeIfMissingPassphrase flag to specify whether missing secret key passphrases should result in postponed decryption - * @return private key if decryption is successful, null if decryption is unsuccessful or postponed - * - * @throws PGPException in case of an OpenPGP error - */ - private PGPPrivateKey tryPublicKeyDecryption( - PGPSecretKeyRing secretKeys, - PGPSecretKey secretKey, - PGPPublicKeyEncryptedData publicKeyEncryptedData, - List> postponed, - boolean postponeIfMissingPassphrase) throws PGPException { - SecretKeyRingProtector protector = options.getSecretKeyProtector(secretKeys); - - if (postponeIfMissingPassphrase && !protector.hasPassphraseFor(secretKey.getKeyID())) { - // Postpone decryption with key with missing passphrase - SubkeyIdentifier identifier = new SubkeyIdentifier(secretKeys, secretKey.getKeyID()); - postponed.add(new Tuple<>(identifier, publicKeyEncryptedData)); - return null; - } - - PGPPrivateKey privateKey = UnlockSecretKey.unlockSecretKey( - secretKey, protector.getDecryptor(secretKey.getKeyID())); - - // test if we have the right private key - PublicKeyDataDecryptorFactory decryptorFactory = ImplementationFactory.getInstance() - .getPublicKeyDataDecryptorFactory(privateKey); - try { - publicKeyEncryptedData.getSymmetricAlgorithm(decryptorFactory); // will only succeed if we have the right secret key - LOGGER.debug("Found correct decryption key {}.", Long.toHexString(secretKey.getKeyID())); - resultBuilder.setDecryptionKey(new SubkeyIdentifier(secretKeys, privateKey.getKeyID())); - return privateKey; - } catch (PGPException | ClassCastException e) { - return null; - } - } - - private InputStream decryptWith(PGPPublicKeyEncryptedData encryptedSessionKey, PGPPrivateKey decryptionKey) - throws PGPException { - if (decryptionKey == null || encryptedSessionKey == null) { - throw new MissingDecryptionMethodException("Decryption failed - No suitable decryption key or passphrase found"); - } - - PublicKeyDataDecryptorFactory dataDecryptor = ImplementationFactory.getInstance() - .getPublicKeyDataDecryptorFactory(decryptionKey); - - PGPSessionKey pgpSessionKey = encryptedSessionKey.getSessionKey(dataDecryptor); - SessionKey sessionKey = new SessionKey(pgpSessionKey); - resultBuilder.setSessionKey(sessionKey); - - SymmetricKeyAlgorithm symmetricKeyAlgorithm = sessionKey.getAlgorithm(); - if (symmetricKeyAlgorithm == SymmetricKeyAlgorithm.NULL) { - LOGGER.debug("Message is unencrypted"); - } else { - LOGGER.debug("Message is encrypted using {}", symmetricKeyAlgorithm); - } - throwIfAlgorithmIsRejected(symmetricKeyAlgorithm); - - integrityProtectedEncryptedInputStream = new IntegrityProtectedInputStream( - encryptedSessionKey.getDataStream(dataDecryptor), encryptedSessionKey, options); - return integrityProtectedEncryptedInputStream; - } - - private void throwIfAlgorithmIsRejected(SymmetricKeyAlgorithm algorithm) - throws UnacceptableAlgorithmException { - if (!PGPainless.getPolicy().getSymmetricKeyDecryptionAlgorithmPolicy().isAcceptable(algorithm)) { - throw new UnacceptableAlgorithmException("Data is " - + (algorithm == SymmetricKeyAlgorithm.NULL ? - "unencrypted" : - "encrypted with symmetric algorithm " + algorithm) + " which is not acceptable as per PGPainless' policy.\n" + - "To mark this algorithm as acceptable, use PGPainless.getPolicy().setSymmetricKeyDecryptionAlgorithmPolicy()."); - } - } - - private void initOnePassSignatures(@Nonnull PGPOnePassSignatureList onePassSignatureList) - throws PGPException { - Iterator iterator = onePassSignatureList.iterator(); - if (!iterator.hasNext()) { - throw new PGPException("Verification failed - No OnePassSignatures found"); - } - - processOnePassSignatures(iterator); - } - - private void processOnePassSignatures(Iterator signatures) - throws PGPException { - while (signatures.hasNext()) { - PGPOnePassSignature signature = signatures.next(); - processOnePassSignature(signature); - } - } - - private void processOnePassSignature(PGPOnePassSignature signature) - throws PGPException { - final long keyId = signature.getKeyID(); - - LOGGER.debug("Encountered OnePassSignature from {}", Long.toHexString(keyId)); - - // Find public key - PGPPublicKeyRing verificationKeyRing = findSignatureVerificationKeyRing(keyId); - if (verificationKeyRing == null) { - onePassSignaturesWithMissingCert.put(keyId, new OnePassSignatureCheck(signature, null)); - return; - } - PGPPublicKey verificationKey = verificationKeyRing.getPublicKey(keyId); - - signature.init(verifierBuilderProvider, verificationKey); - OnePassSignatureCheck onePassSignature = new OnePassSignatureCheck(signature, verificationKeyRing); - onePassSignatureChecks.add(onePassSignature); - } - - private PGPSecretKeyRing findDecryptionKeyRing(long keyId) { - for (PGPSecretKeyRing key : options.getDecryptionKeys()) { - if (key.getSecretKey(keyId) != null) { - return key; - } - } - return null; - } - - private PGPPublicKeyRing findSignatureVerificationKeyRing(long keyId) { - PGPPublicKeyRing verificationKeyRing = null; - for (PGPPublicKeyRing publicKeyRing : options.getCertificates()) { - PGPPublicKey verificationKey = publicKeyRing.getPublicKey(keyId); - if (verificationKey != null) { - LOGGER.debug("Found public key {} for signature verification", Long.toHexString(keyId)); - verificationKeyRing = publicKeyRing; - break; - } - } - - if (verificationKeyRing == null && options.getMissingCertificateCallback() != null) { - verificationKeyRing = options.getMissingCertificateCallback().onMissingPublicKeyEncountered(keyId); - } - - return verificationKeyRing; - } -} From 2a4bc035555af8e998b355dc85ff5b987995b158 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 24 Oct 2022 17:49:30 +0200 Subject: [PATCH 49/80] Only check message integrity once --- .../IntegrityProtectedInputStream.java | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/IntegrityProtectedInputStream.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/IntegrityProtectedInputStream.java index 4da52d0f..286160e8 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/IntegrityProtectedInputStream.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/IntegrityProtectedInputStream.java @@ -11,12 +11,17 @@ import javax.annotation.Nonnull; import org.bouncycastle.openpgp.PGPEncryptedData; import org.bouncycastle.openpgp.PGPException; import org.pgpainless.exception.ModificationDetectionException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; public class IntegrityProtectedInputStream extends InputStream { + private static final Logger LOGGER = LoggerFactory.getLogger(IntegrityProtectedInputStream.class); + private final InputStream inputStream; private final PGPEncryptedData encryptedData; private final ConsumerOptions options; + private boolean closed = false; public IntegrityProtectedInputStream(InputStream inputStream, PGPEncryptedData encryptedData, ConsumerOptions options) { this.inputStream = inputStream; @@ -36,11 +41,17 @@ public class IntegrityProtectedInputStream extends InputStream { @Override public void close() throws IOException { + if (closed) { + return; + } + closed = true; + if (encryptedData.isIntegrityProtected() && !options.isIgnoreMDCErrors()) { try { if (!encryptedData.verify()) { throw new ModificationDetectionException(); } + LOGGER.debug("Integrity Protection check passed"); } catch (PGPException e) { throw new IOException("Failed to verify integrity protection", e); } From 4c6edc067bf8ffbe83c9b2519339d490ba31366c Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 24 Oct 2022 17:56:33 +0200 Subject: [PATCH 50/80] Fix more tests --- .../OpenPgpMessageInputStream.java | 76 ++++++++++++++----- .../TeeBCPGInputStream.java | 1 - 2 files changed, 59 insertions(+), 18 deletions(-) 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 dee59058..de80f7ff 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 @@ -4,7 +4,6 @@ package org.pgpainless.decryption_verification; -import java.io.BufferedInputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; @@ -37,7 +36,6 @@ 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.util.io.Streams; import org.bouncycastle.util.io.TeeInputStream; import org.pgpainless.PGPainless; import org.pgpainless.algorithm.CompressionAlgorithm; @@ -210,6 +208,7 @@ public class OpenPgpMessageInputStream extends DecryptionStream { } switch (type) { + case standard: // tee out packet bytes for signature verification packetInputStream = new TeeBCPGInputStream(BCPGInputStream.wrap(inputStream), this.signatures); @@ -217,20 +216,22 @@ public class OpenPgpMessageInputStream extends DecryptionStream { // *omnomnom* consumePackets(); break; + case cleartext_signed: + resultBuilder.setCleartextSigned(); MultiPassStrategy multiPassStrategy = options.getMultiPassStrategy(); PGPSignatureList detachedSignatures = ClearsignedMessageUtil .detachSignaturesFromInbandClearsignedMessage( inputStream, multiPassStrategy.getMessageOutputStream()); for (PGPSignature signature : detachedSignatures) { - options.addVerificationOfDetachedSignature(signature); + signatures.addDetachedSignature(signature); } options.forceNonOpenPgpData(); - packetInputStream = null; nestedInputStream = new TeeInputStream(multiPassStrategy.getMessageInputStream(), this.signatures); break; + case non_openpgp: packetInputStream = null; nestedInputStream = new TeeInputStream(inputStream, this.signatures); @@ -348,14 +349,14 @@ public class OpenPgpMessageInputStream extends DecryptionStream { metadata.depth + 1); LOGGER.debug("Compressed Data Packet (" + compressionLayer.algorithm + ") at depth " + metadata.depth + " encountered"); InputStream decompressed = compressedData.getDataStream(); - nestedInputStream = new OpenPgpMessageInputStream(buffer(decompressed), options, compressionLayer, policy); + nestedInputStream = new OpenPgpMessageInputStream(decompressed, options, compressionLayer, policy); } private void processOnePassSignature() throws PGPException, IOException { syntaxVerifier.next(InputAlphabet.OnePassSignature); PGPOnePassSignature onePassSignature = packetInputStream.readOnePassSignature(); LOGGER.debug("One-Pass-Signature Packet by key " + KeyIdUtil.formatKeyId(onePassSignature.getKeyID()) + - "at depth " + metadata.depth + " encountered"); + " at depth " + metadata.depth + " encountered"); signatures.addOnePassSignature(onePassSignature); } @@ -434,7 +435,7 @@ public class OpenPgpMessageInputStream extends DecryptionStream { InputStream decrypted = skesk.getDataStream(decryptorFactory); encryptedData.sessionKey = sessionKey; IntegrityProtectedInputStream integrityProtected = new IntegrityProtectedInputStream(decrypted, skesk, options); - nestedInputStream = new OpenPgpMessageInputStream(buffer(integrityProtected), options, encryptedData, policy); + nestedInputStream = new OpenPgpMessageInputStream(integrityProtected, options, encryptedData, policy); LOGGER.debug("Successfully decrypted data with provided session key"); return true; } else if (esk instanceof PGPPublicKeyEncryptedData) { @@ -442,7 +443,7 @@ public class OpenPgpMessageInputStream extends DecryptionStream { InputStream decrypted = pkesk.getDataStream(decryptorFactory); encryptedData.sessionKey = sessionKey; IntegrityProtectedInputStream integrityProtected = new IntegrityProtectedInputStream(decrypted, pkesk, options); - nestedInputStream = new OpenPgpMessageInputStream(buffer(integrityProtected), options, encryptedData, policy); + nestedInputStream = new OpenPgpMessageInputStream(integrityProtected, options, encryptedData, policy); LOGGER.debug("Successfully decrypted data with provided session key"); return true; } else { @@ -579,7 +580,7 @@ public class OpenPgpMessageInputStream extends DecryptionStream { encryptedData.sessionKey = sessionKey; LOGGER.debug("Successfully decrypted data with passphrase"); IntegrityProtectedInputStream integrityProtected = new IntegrityProtectedInputStream(decrypted, skesk, options); - nestedInputStream = new OpenPgpMessageInputStream(buffer(integrityProtected), options, encryptedData, policy); + nestedInputStream = new OpenPgpMessageInputStream(integrityProtected, options, encryptedData, policy); return true; } catch (UnacceptableAlgorithmException e) { throw e; @@ -606,7 +607,7 @@ public class OpenPgpMessageInputStream extends DecryptionStream { LOGGER.debug("Successfully decrypted data with key " + decryptionKeyId); IntegrityProtectedInputStream integrityProtected = new IntegrityProtectedInputStream(decrypted, pkesk, options); - nestedInputStream = new OpenPgpMessageInputStream(buffer(integrityProtected), options, encryptedData, policy); + nestedInputStream = new OpenPgpMessageInputStream(integrityProtected, options, encryptedData, policy); return true; } catch (UnacceptableAlgorithmException e) { throw e; @@ -631,10 +632,6 @@ public class OpenPgpMessageInputStream extends DecryptionStream { } } - private static InputStream buffer(InputStream inputStream) { - return new BufferedInputStream(inputStream); - } - private List> findPotentialDecryptionKeys(PGPPublicKeyEncryptedData pkesk) { int algorithm = pkesk.getAlgorithm(); List> decryptionKeyCandidates = new ArrayList<>(); @@ -665,7 +662,9 @@ public class OpenPgpMessageInputStream extends DecryptionStream { @Override public int read() throws IOException { if (nestedInputStream == null) { - syntaxVerifier.assertValid(); + if (packetInputStream != null) { + syntaxVerifier.assertValid(); + } return -1; } @@ -699,7 +698,6 @@ public class OpenPgpMessageInputStream extends DecryptionStream { @Override public int read(@Nonnull byte[] b, int off, int len) throws IOException { - if (nestedInputStream == null) { if (packetInputStream != null) { syntaxVerifier.assertValid(); @@ -768,6 +766,7 @@ public class OpenPgpMessageInputStream extends DecryptionStream { if (!closed) { throw new IllegalStateException("Stream must be closed before access to metadata can be granted."); } + return new MessageMetadata((MessageMetadata.Message) metadata); } @@ -857,6 +856,9 @@ public class OpenPgpMessageInputStream extends DecryptionStream { final Stack> opsUpdateStack; List literalOPS = new ArrayList<>(); final List correspondingSignatures; + final List prependedSignaturesWithMissingCert = new ArrayList<>(); + final List inbandSignaturesWithMissingCert = new ArrayList<>(); + final List detachedSignaturesWithMissingCert = new ArrayList<>(); boolean isLiteral = true; private Signatures(ConsumerOptions options) { @@ -876,15 +878,29 @@ public class OpenPgpMessageInputStream extends DecryptionStream { void addDetachedSignature(PGPSignature signature) { SignatureCheck check = initializeSignature(signature); + long keyId = SignatureUtils.determineIssuerKeyId(signature); if (check != null) { detachedSignatures.add(check); + } else { + LOGGER.debug("No suitable certificate for verification of signature by key " + KeyIdUtil.formatKeyId(keyId) + " found."); + this.detachedSignaturesWithMissingCert.add(new SignatureVerification.Failure( + new SignatureVerification(signature, null), + new SignatureValidationException("Missing verification key") + )); } } void addPrependedSignature(PGPSignature signature) { SignatureCheck check = initializeSignature(signature); + long keyId = SignatureUtils.determineIssuerKeyId(signature); if (check != null) { this.prependedSignatures.add(check); + } else { + LOGGER.debug("No suitable certificate for verification of signature by key " + KeyIdUtil.formatKeyId(keyId) + " found."); + this.prependedSignaturesWithMissingCert.add(new SignatureVerification.Failure( + new SignatureVerification(signature, null), + new SignatureValidationException("Missing verification key") + )); } } @@ -916,11 +932,14 @@ public class OpenPgpMessageInputStream extends DecryptionStream { } void addCorrespondingOnePassSignature(PGPSignature signature, MessageMetadata.Layer layer, Policy policy) { + boolean found = false; + long keyId = SignatureUtils.determineIssuerKeyId(signature); for (int i = onePassSignatures.size() - 1; i >= 0; i--) { OnePassSignatureCheck onePassSignature = onePassSignatures.get(i); - if (onePassSignature.getOnePassSignature().getKeyID() != signature.getKeyID()) { + if (onePassSignature.getOnePassSignature().getKeyID() != keyId) { continue; } + found = true; if (onePassSignature.getSignature() != null) { continue; @@ -942,6 +961,13 @@ public class OpenPgpMessageInputStream extends DecryptionStream { } break; } + + if (!found) { + LOGGER.debug("No suitable certificate for verification of signature by key " + KeyIdUtil.formatKeyId(keyId) + " found."); + inbandSignaturesWithMissingCert.add(new SignatureVerification.Failure( + new SignatureVerification(signature, null), + new SignatureValidationException("Missing verification key."))); + } } void enterNesting() { @@ -983,6 +1009,10 @@ public class OpenPgpMessageInputStream extends DecryptionStream { return cert; } } + + if (options.getMissingCertificateCallback() != null) { + return options.getMissingCertificateCallback().onMissingPublicKeyEncountered(keyId); + } return null; // TODO: Missing cert for sig } @@ -1066,6 +1096,18 @@ public class OpenPgpMessageInputStream extends DecryptionStream { layer.addRejectedPrependedSignature(new SignatureVerification.Failure(verification, e)); } } + + for (SignatureVerification.Failure rejected : inbandSignaturesWithMissingCert) { + layer.addRejectedOnePassSignature(rejected); + } + + for (SignatureVerification.Failure rejected : prependedSignaturesWithMissingCert) { + layer.addRejectedPrependedSignature(rejected); + } + + for (SignatureVerification.Failure rejected : detachedSignaturesWithMissingCert) { + layer.addRejectedDetachedSignature(rejected); + } } @Override diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/TeeBCPGInputStream.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/TeeBCPGInputStream.java index 1ca1b3f9..bbcf593e 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/TeeBCPGInputStream.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/TeeBCPGInputStream.java @@ -98,7 +98,6 @@ public class TeeBCPGInputStream { public void close() throws IOException { this.packetInputStream.close(); - this.delayedTee.close(); } public static class DelayedTeeInputStreamInputStream extends InputStream { From 160a5fe8ff75f559bd02d7cf42a85a52e5858892 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 24 Oct 2022 18:30:40 +0200 Subject: [PATCH 51/80] Fix last two broken tests --- .../OpenPgpMessageInputStream.java | 31 ++++++++++++------- .../org/pgpainless/key/util/KeyRingUtils.java | 11 +++++++ 2 files changed, 30 insertions(+), 12 deletions(-) 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 de80f7ff..11a61e8c 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 @@ -9,6 +9,7 @@ import java.io.InputStream; import java.io.OutputStream; import java.util.ArrayList; import java.util.Collection; +import java.util.Date; import java.util.HashSet; import java.util.List; import java.util.Set; @@ -60,12 +61,13 @@ import org.pgpainless.key.info.KeyRingInfo; import org.pgpainless.key.protection.SecretKeyRingProtector; import org.pgpainless.key.protection.UnlockSecretKey; import org.pgpainless.key.util.KeyIdUtil; +import org.pgpainless.key.util.KeyRingUtils; import org.pgpainless.policy.Policy; import org.pgpainless.signature.SignatureUtils; +import org.pgpainless.signature.consumer.CertificateValidator; import org.pgpainless.signature.consumer.OnePassSignatureCheck; import org.pgpainless.signature.consumer.SignatureCheck; import org.pgpainless.signature.consumer.SignatureValidator; -import org.pgpainless.signature.consumer.SignatureVerifier; import org.pgpainless.util.ArmoredInputStreamFactory; import org.pgpainless.util.Passphrase; import org.pgpainless.util.SessionKey; @@ -654,7 +656,16 @@ public class OpenPgpMessageInputStream extends DecryptionStream { if (decryptionKey == null) { continue; } - return secretKeys; + + KeyRingInfo info = new KeyRingInfo(secretKeys, policy, new Date()); + List encryptionKeys = info.getEncryptionSubkeys(EncryptionPurpose.ANY); + for (PGPPublicKey key : encryptionKeys) { + if (key.getKeyID() == keyID) { + return secretKeys; + } + } + + LOGGER.debug("Subkey " + Long.toHexString(keyID) + " cannot be used for decryption."); } return null; } @@ -951,8 +962,8 @@ public class OpenPgpMessageInputStream extends DecryptionStream { try { SignatureValidator.signatureWasCreatedInBounds(options.getVerifyNotBefore(), options.getVerifyNotAfter()) - .verify(signature); - SignatureVerifier.verifyOnePassSignature(signature, onePassSignature.getVerificationKeys().getPublicKey(signature.getKeyID()), onePassSignature, policy); + .verify(signature); + CertificateValidator.validateCertificateAndVerifyOnePassSignature(onePassSignature, policy); LOGGER.debug("Acceptable signature by key " + verification.getSigningKey()); layer.addVerifiedOnePassSignature(verification); } catch (SignatureValidationException e) { @@ -1068,10 +1079,8 @@ public class OpenPgpMessageInputStream extends DecryptionStream { try { SignatureValidator.signatureWasCreatedInBounds(options.getVerifyNotBefore(), options.getVerifyNotAfter()) .verify(detached.getSignature()); - SignatureVerifier.verifyInitializedSignature( - detached.getSignature(), - detached.getSigningKeyRing().getPublicKey(detached.getSigningKeyIdentifier().getKeyId()), - policy, detached.getSignature().getCreationTime()); + CertificateValidator.validateCertificateAndVerifyInitializedSignature( + detached.getSignature(), KeyRingUtils.publicKeys(detached.getSigningKeyRing()), policy); LOGGER.debug("Acceptable signature by key " + verification.getSigningKey()); layer.addVerifiedDetachedSignature(verification); } catch (SignatureValidationException e) { @@ -1085,10 +1094,8 @@ public class OpenPgpMessageInputStream extends DecryptionStream { try { SignatureValidator.signatureWasCreatedInBounds(options.getVerifyNotBefore(), options.getVerifyNotAfter()) .verify(prepended.getSignature()); - SignatureVerifier.verifyInitializedSignature( - prepended.getSignature(), - prepended.getSigningKeyRing().getPublicKey(prepended.getSigningKeyIdentifier().getKeyId()), - policy, prepended.getSignature().getCreationTime()); + CertificateValidator.validateCertificateAndVerifyInitializedSignature( + prepended.getSignature(), KeyRingUtils.publicKeys(prepended.getSigningKeyRing()), policy); LOGGER.debug("Acceptable signature by key " + verification.getSigningKey()); layer.addVerifiedPrependedSignature(verification); } catch (SignatureValidationException e) { diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/util/KeyRingUtils.java b/pgpainless-core/src/main/java/org/pgpainless/key/util/KeyRingUtils.java index 013e283e..2ce058fd 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/key/util/KeyRingUtils.java +++ b/pgpainless-core/src/main/java/org/pgpainless/key/util/KeyRingUtils.java @@ -144,6 +144,17 @@ public final class KeyRingUtils { return secretKey; } + @Nonnull + public static PGPPublicKeyRing publicKeys(@Nonnull PGPKeyRing keys) { + if (keys instanceof PGPPublicKeyRing) { + return (PGPPublicKeyRing) keys; + } else if (keys instanceof PGPSecretKeyRing) { + return publicKeyRingFrom((PGPSecretKeyRing) keys); + } else { + throw new IllegalArgumentException("Unknown keys class: " + keys.getClass().getName()); + } + } + /** * Extract a {@link PGPPublicKeyRing} containing all public keys from the provided {@link PGPSecretKeyRing}. * From 08ceee9243af8b5a6c24a131720b39193293b648 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 24 Oct 2022 18:32:56 +0200 Subject: [PATCH 52/80] Wrap MalformedOpenPgpMessageException in BadData --- .../decryption_verification/OpenPgpMessageInputStream.java | 2 +- .../src/main/java/org/pgpainless/sop/DecryptImpl.java | 3 ++- .../src/main/java/org/pgpainless/sop/DetachedVerifyImpl.java | 3 ++- .../src/main/java/org/pgpainless/sop/InlineVerifyImpl.java | 3 ++- 4 files changed, 7 insertions(+), 4 deletions(-) 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 11a61e8c..f081e021 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 @@ -656,7 +656,7 @@ public class OpenPgpMessageInputStream extends DecryptionStream { if (decryptionKey == null) { continue; } - + KeyRingInfo info = new KeyRingInfo(secretKeys, policy, new Date()); List encryptionKeys = info.getEncryptionSubkeys(EncryptionPurpose.ANY); for (PGPPublicKey key : encryptionKeys) { diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/DecryptImpl.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/DecryptImpl.java index 4957f748..68be2793 100644 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/DecryptImpl.java +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/DecryptImpl.java @@ -23,6 +23,7 @@ import org.pgpainless.decryption_verification.ConsumerOptions; import org.pgpainless.decryption_verification.DecryptionStream; import org.pgpainless.decryption_verification.OpenPgpMetadata; import org.pgpainless.decryption_verification.SignatureVerification; +import org.pgpainless.exception.MalformedOpenPgpMessageException; import org.pgpainless.exception.MissingDecryptionMethodException; import org.pgpainless.exception.WrongPassphraseException; import org.pgpainless.util.Passphrase; @@ -135,7 +136,7 @@ public class DecryptImpl implements Decrypt { throw new SOPGPException.CannotDecrypt(); } catch (WrongPassphraseException e) { throw new SOPGPException.KeyIsProtected(); - } catch (PGPException | IOException e) { + } catch (MalformedOpenPgpMessageException | PGPException | IOException e) { throw new SOPGPException.BadData(e); } finally { // Forget passphrases after decryption diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/DetachedVerifyImpl.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/DetachedVerifyImpl.java index e6e2768e..52ba616f 100644 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/DetachedVerifyImpl.java +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/DetachedVerifyImpl.java @@ -18,6 +18,7 @@ import org.pgpainless.decryption_verification.ConsumerOptions; import org.pgpainless.decryption_verification.DecryptionStream; import org.pgpainless.decryption_verification.OpenPgpMetadata; import org.pgpainless.decryption_verification.SignatureVerification; +import org.pgpainless.exception.MalformedOpenPgpMessageException; import sop.Verification; import sop.exception.SOPGPException; import sop.operation.DetachedVerify; @@ -85,7 +86,7 @@ public class DetachedVerifyImpl implements DetachedVerify { } return verificationList; - } catch (PGPException e) { + } catch (MalformedOpenPgpMessageException | PGPException e) { throw new SOPGPException.BadData(e); } } diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/InlineVerifyImpl.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/InlineVerifyImpl.java index fe754e3b..66f5d1aa 100644 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/InlineVerifyImpl.java +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/InlineVerifyImpl.java @@ -19,6 +19,7 @@ import org.pgpainless.decryption_verification.ConsumerOptions; import org.pgpainless.decryption_verification.DecryptionStream; import org.pgpainless.decryption_verification.OpenPgpMetadata; import org.pgpainless.decryption_verification.SignatureVerification; +import org.pgpainless.exception.MalformedOpenPgpMessageException; import sop.ReadyWithResult; import sop.Verification; import sop.exception.SOPGPException; @@ -84,7 +85,7 @@ public class InlineVerifyImpl implements InlineVerify { } return verificationList; - } catch (PGPException e) { + } catch (MalformedOpenPgpMessageException | PGPException e) { throw new SOPGPException.BadData(e); } } From 651ca93f9066dcf6c45359c3bcdfc01963ec4939 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 24 Oct 2022 18:38:30 +0200 Subject: [PATCH 53/80] Add missing REUSE license headers --- .../pgpainless/decryption_verification/DecryptionStream.java | 4 ++++ .../OpenPgpMessageInputStreamTest.java | 4 ++++ .../decryption_verification/syntax_check/PDATest.java | 4 ++++ 3 files changed, 12 insertions(+) diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionStream.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionStream.java index c531f487..0f221ad7 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionStream.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionStream.java @@ -1,3 +1,7 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + package org.pgpainless.decryption_verification; import javax.annotation.Nonnull; 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 f9539149..d2c62fb2 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,3 +1,7 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + package org.pgpainless.decryption_verification; import static org.junit.jupiter.api.Assertions.assertEquals; diff --git a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/syntax_check/PDATest.java b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/syntax_check/PDATest.java index 8fa107f6..9250acfa 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/syntax_check/PDATest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/syntax_check/PDATest.java @@ -1,3 +1,7 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + package org.pgpainless.decryption_verification.syntax_check; import static org.junit.jupiter.api.Assertions.assertThrows; From d8ff266406efb201a607959f498e6be4d909f4a8 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 24 Oct 2022 19:23:52 +0200 Subject: [PATCH 54/80] Handle unknown packet versions gracefully --- .../OpenPgpMessageInputStream.java | 9 +- .../UnsupportedPacketVersionsTest.java | 410 ++++++++++++++++++ 2 files changed, 418 insertions(+), 1 deletion(-) create mode 100644 pgpainless-core/src/test/java/org/pgpainless/decryption_verification/UnsupportedPacketVersionsTest.java 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 f081e021..75b78294 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 @@ -18,6 +18,7 @@ import javax.annotation.Nonnull; import org.bouncycastle.bcpg.ArmoredInputStream; import org.bouncycastle.bcpg.BCPGInputStream; +import org.bouncycastle.bcpg.UnsupportedPacketVersionException; import org.bouncycastle.openpgp.PGPCompressedData; import org.bouncycastle.openpgp.PGPEncryptedData; import org.bouncycastle.openpgp.PGPEncryptedDataList; @@ -366,7 +367,13 @@ public class OpenPgpMessageInputStream extends DecryptionStream { // true if Signature corresponds to OnePassSignature boolean isSigForOPS = syntaxVerifier.peekStack() == StackAlphabet.ops; syntaxVerifier.next(InputAlphabet.Signature); - PGPSignature signature = packetInputStream.readSignature(); + PGPSignature signature; + try { + signature = packetInputStream.readSignature(); + } catch (UnsupportedPacketVersionException e) { + LOGGER.debug("Unsupported Signature at depth " + metadata.depth + " encountered.", e); + return; + } long keyId = SignatureUtils.determineIssuerKeyId(signature); if (isSigForOPS) { LOGGER.debug("Signature Packet corresponding to One-Pass-Signature by key " + diff --git a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/UnsupportedPacketVersionsTest.java b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/UnsupportedPacketVersionsTest.java new file mode 100644 index 00000000..68916348 --- /dev/null +++ b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/UnsupportedPacketVersionsTest.java @@ -0,0 +1,410 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.decryption_verification; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.nio.charset.Charset; + +import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.PGPPublicKeyRing; +import org.bouncycastle.openpgp.PGPSecretKeyRing; +import org.bouncycastle.util.io.Streams; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.pgpainless.PGPainless; + +public class UnsupportedPacketVersionsTest { + + private static final String KEY = "-----BEGIN PGP PRIVATE KEY BLOCK-----\n" + + "Comment: Bob's OpenPGP Transferable Secret Key\n" + + "\n" + + "lQVYBF2lnPIBDAC5cL9PQoQLTMuhjbYvb4Ncuuo0bfmgPRFywX53jPhoFf4Zg6mv\n" + + "/seOXpgecTdOcVttfzC8ycIKrt3aQTiwOG/ctaR4Bk/t6ayNFfdUNxHWk4WCKzdz\n" + + "/56fW2O0F23qIRd8UUJp5IIlN4RDdRCtdhVQIAuzvp2oVy/LaS2kxQoKvph/5pQ/\n" + + "5whqsyroEWDJoSV0yOb25B/iwk/pLUFoyhDG9bj0kIzDxrEqW+7Ba8nocQlecMF3\n" + + "X5KMN5kp2zraLv9dlBBpWW43XktjcCZgMy20SouraVma8Je/ECwUWYUiAZxLIlMv\n" + + "9CurEOtxUw6N3RdOtLmYZS9uEnn5y1UkF88o8Nku890uk6BrewFzJyLAx5wRZ4F0\n" + + "qV/yq36UWQ0JB/AUGhHVPdFf6pl6eaxBwT5GXvbBUibtf8YI2og5RsgTWtXfU7eb\n" + + "SGXrl5ZMpbA6mbfhd0R8aPxWfmDWiIOhBufhMCvUHh1sApMKVZnvIff9/0Dca3wb\n" + + "vLIwa3T4CyshfT0AEQEAAQAL/RZqbJW2IqQDCnJi4Ozm++gPqBPiX1RhTWSjwxfM\n" + + "cJKUZfzLj414rMKm6Jh1cwwGY9jekROhB9WmwaaKT8HtcIgrZNAlYzANGRCM4TLK\n" + + "3VskxfSwKKna8l+s+mZglqbAjUg3wmFuf9Tj2xcUZYmyRm1DEmcN2ZzpvRtHgX7z\n" + + "Wn1mAKUlSDJZSQks0zjuMNbupcpyJokdlkUg2+wBznBOTKzgMxVNC9b2g5/tMPUs\n" + + "hGGWmF1UH+7AHMTaS6dlmr2ZBIyogdnfUqdNg5sZwsxSNrbglKP4sqe7X61uEAIQ\n" + + "bD7rT3LonLbhkrj3I8wilUD8usIwt5IecoHhd9HziqZjRCc1BUBkboUEoyedbDV4\n" + + "i4qfsFZ6CEWoLuD5pW7dEp0M+WeuHXO164Rc+LnH6i1VQrpb1Okl4qO6ejIpIjBI\n" + + "1t3GshtUu/mwGBBxs60KBX5g77mFQ9lLCRj8lSYqOsHRKBhUp4qM869VA+fD0BRP\n" + + "fqPT0I9IH4Oa/A3jYJcg622GwQYA1LhnP208Waf6PkQSJ6kyr8ymY1yVh9VBE/g6\n" + + "fRDYA+pkqKnw9wfH2Qho3ysAA+OmVOX8Hldg+Pc0Zs0e5pCavb0En8iFLvTA0Q2E\n" + + "LR5rLue9uD7aFuKFU/VdcddY9Ww/vo4k5p/tVGp7F8RYCFn9rSjIWbfvvZi1q5Tx\n" + + "+akoZbga+4qQ4WYzB/obdX6SCmi6BndcQ1QdjCCQU6gpYx0MddVERbIp9+2SXDyL\n" + + "hpxjSyz+RGsZi/9UAshT4txP4+MZBgDfK3ZqtW+h2/eMRxkANqOJpxSjMyLO/FXN\n" + + "WxzTDYeWtHNYiAlOwlQZEPOydZFty9IVzzNFQCIUCGjQ/nNyhw7adSgUk3+BXEx/\n" + + "MyJPYY0BYuhLxLYcrfQ9nrhaVKxRJj25SVHj2ASsiwGJRZW4CC3uw40OYxfKEvNC\n" + + "mer/VxM3kg8qqGf9KUzJ1dVdAvjyx2Hz6jY2qWCyRQ6IMjWHyd43C4r3jxooYKUC\n" + + "YnstRQyb/gCSKahveSEjo07CiXMr88UGALwzEr3npFAsPW3osGaFLj49y1oRe11E\n" + + "he9gCHFm+fuzbXrWmdPjYU5/ZdqdojzDqfu4ThfnipknpVUM1o6MQqkjM896FHm8\n" + + "zbKVFSMhEP6DPHSCexMFrrSgN03PdwHTO6iBaIBBFqmGY01tmJ03SxvSpiBPON9P\n" + + "NVvy/6UZFedTq8A07OUAxO62YUSNtT5pmK2vzs3SAZJmbFbMh+NN204TRI72GlqT\n" + + "t5hcfkuv8hrmwPS/ZR6q312mKQ6w/1pqO9qitCFCb2IgQmFiYmFnZSA8Ym9iQG9w\n" + + "ZW5wZ3AuZXhhbXBsZT6JAc4EEwEKADgCGwMFCwkIBwIGFQoJCAsCBBYCAwECHgEC\n" + + "F4AWIQTRpm4aI7GCyZgPeIz7/MgqAV5zMAUCXaWe+gAKCRD7/MgqAV5zMG9sC/9U\n" + + "2T3RrqEbw533FPNfEflhEVRIZ8gDXKM8hU6cqqEzCmzZT6xYTe6sv4y+PJBGXJFX\n" + + "yhj0g6FDkSyboM5litOcTupURObVqMgA/Y4UKERznm4fzzH9qek85c4ljtLyNufe\n" + + "doL2pp3vkGtn7eD0QFRaLLmnxPKQ/TlZKdLE1G3u8Uot8QHicaR6GnAdc5UXQJE3\n" + + "BiV7jZuDyWmZ1cUNwJkKL6oRtp+ZNDOQCrLNLecKHcgCqrpjSQG5oouba1I1Q6Vl\n" + + "sP44dhA1nkmLHtxlTOzpeHj4jnk1FaXmyasurrrI5CgU/L2Oi39DGKTH/A/cywDN\n" + + "4ZplIQ9zR8enkbXquUZvFDe+Xz+6xRXtb5MwQyWODB3nHw85HocLwRoIN9WdQEI+\n" + + "L8a/56AuOwhs8llkSuiITjR7r9SgKJC2WlAHl7E8lhJ3VDW3ELC56KH308d6mwOG\n" + + "ZRAqIAKzM1T5FGjMBhq7ZV0eqdEntBh3EcOIfj2M8rg1MzJv+0mHZOIjByawikad\n" + + "BVgEXaWc8gEMANYwv1xsYyunXYK0X1vY/rP1NNPvhLyLIE7NpK90YNBj+xS1ldGD\n" + + "bUdZqZeef2xJe8gMQg05DoD1DF3GipZ0Ies65beh+d5hegb7N4pzh0LzrBrVNHar\n" + + "29b5ExdI7i4iYD5TO6Vr/qTUOiAN/byqELEzAb+L+b2DVz/RoCm4PIp1DU9ewcc2\n" + + "WB38Ofqut3nLYA5tqJ9XvAiEQme+qAVcM3ZFcaMt4I4dXhDZZNg+D9LiTWcxdUPB\n" + + "leu8iwDRjAgyAhPzpFp+nWoqWA81uIiULWD1Fj+IVoY3ZvgivoYOiEFBJ9lbb4te\n" + + "g9m5UT/AaVDTWuHzbspVlbiVe+qyB77C2daWzNyx6UYBPLOo4r0t0c91kbNE5lgj\n" + + "Z7xz6los0N1U8vq91EFSeQJoSQ62XWavYmlCLmdNT6BNfgh4icLsT7Vr1QMX9jzn\n" + + "JtTPxdXytSdHvpSpULsqJ016l0dtmONcK3z9mj5N5z0k1tg1AH970TGYOe2aUcSx\n" + + "IRDMXDOPyzEfjwARAQABAAv9F2CwsjS+Sjh1M1vegJbZjei4gF1HHpEM0K0PSXsp\n" + + "SfVvpR4AoSJ4He6CXSMWg0ot8XKtDuZoV9jnJaES5UL9pMAD7JwIOqZm/DYVJM5h\n" + + "OASCh1c356/wSbFbzRHPtUdZO9Q30WFNJM5pHbCJPjtNoRmRGkf71RxtvHBzy7np\n" + + "Ga+W6U/NVKHw0i0CYwMI0YlKDakYW3Pm+QL+gHZFvngGweTod0f9l2VLLAmeQR/c\n" + + "+EZs7lNumhuZ8mXcwhUc9JQIhOkpO+wreDysEFkAcsKbkQP3UDUsA1gFx9pbMzT0\n" + + "tr1oZq2a4QBtxShHzP/ph7KLpN+6qtjks3xB/yjTgaGmtrwM8tSe0wD1RwXS+/1o\n" + + "BHpXTnQ7TfeOGUAu4KCoOQLv6ELpKWbRBLWuiPwMdbGpvVFALO8+kvKAg9/r+/ny\n" + + "zM2GQHY+J3Jh5JxPiJnHfXNZjIKLbFbIPdSKNyJBuazXW8xIa//mEHMI5OcvsZBK\n" + + "clAIp7LXzjEjKXIwHwDcTn9pBgDpdOKTHOtJ3JUKx0rWVsDH6wq6iKV/FTVSY5jl\n" + + "zN+puOEsskF1Lfxn9JsJihAVO3yNsp6RvkKtyNlFazaCVKtDAmkjoh60XNxcNRqr\n" + + "gCnwdpbgdHP6v/hvZY54ZaJjz6L2e8unNEkYLxDt8cmAyGPgH2XgL7giHIp9jrsQ\n" + + "aS381gnYwNX6wE1aEikgtY91nqJjwPlibF9avSyYQoMtEqM/1UjTjB2KdD/MitK5\n" + + "fP0VpvuXpNYZedmyq4UOMwdkiNMGAOrfmOeT0olgLrTMT5H97Cn3Yxbk13uXHNu/\n" + + "ZUZZNe8s+QtuLfUlKAJtLEUutN33TlWQY522FV0m17S+b80xJib3yZVJteVurrh5\n" + + "HSWHAM+zghQAvCesg5CLXa2dNMkTCmZKgCBvfDLZuZbjFwnwCI6u/NhOY9egKuUf\n" + + "SA/je/RXaT8m5VxLYMxwqQXKApzD87fv0tLPlVIEvjEsaf992tFEFSNPcG1l/jpd\n" + + "5AVXw6kKuf85UkJtYR1x2MkQDrqY1QX/XMw00kt8y9kMZUre19aCArcmor+hDhRJ\n" + + "E3Gt4QJrD9z/bICESw4b4z2DbgD/Xz9IXsA/r9cKiM1h5QMtXvuhyfVeM01enhxM\n" + + "GbOH3gjqqGNKysx0UODGEwr6AV9hAd8RWXMchJLaExK9J5SRawSg671ObAU24SdY\n" + + "vMQ9Z4kAQ2+1ReUZzf3ogSMRZtMT+d18gT6L90/y+APZIaoArLPhebIAGq39HLmJ\n" + + "26x3z0WAgrpA1kNsjXEXkoiZGPLKIGoe3hqJAbYEGAEKACAWIQTRpm4aI7GCyZgP\n" + + "eIz7/MgqAV5zMAUCXaWc8gIbDAAKCRD7/MgqAV5zMOn/C/9ugt+HZIwX308zI+QX\n" + + "c5vDLReuzmJ3ieE0DMO/uNSC+K1XEioSIZP91HeZJ2kbT9nn9fuReuoff0T0Dief\n" + + "rbwcIQQHFFkrqSp1K3VWmUGp2JrUsXFVdjy/fkBIjTd7c5boWljv/6wAsSfiv2V0\n" + + "JSM8EFU6TYXxswGjFVfc6X97tJNeIrXL+mpSmPPqy2bztcCCHkWS5lNLWQw+R7Vg\n" + + "71Fe6yBSNVrqC2/imYG2J9zlowjx1XU63Wdgqp2Wxt0l8OmsB/W80S1fRF5G4SDH\n" + + "s9HXglXXqPsBRZJYfP+VStm9L5P/sKjCcX6WtZR7yS6G8zj/X767MLK/djANvpPd\n" + + "NVniEke6hM3CNBXYPAMhQBMWhCulcoz+0lxi8L34rMN+Dsbma96psdUrn7uLaB91\n" + + "6we0CTfF8qqm7BsVAgalon/UUiuMY80U3ueoj3okiSTiHIjD/YtpXSPioC8nMng7\n" + + "xqAY9Bwizt4FWgXuLm1a4+So4V9j1TRCXd12Uc2l2RNmgDE=\n" + + "=miES\n" + + "-----END PGP PRIVATE KEY BLOCK-----\n"; + + private static final String PKESK3_PKESK23_SEIP = "-----BEGIN PGP MESSAGE-----\n" + + "\n" + + "wcDMA3wvqk35PDeyAQv/YP+fDWtifT7KSk+tWrgbyvsYCt5Wh0IPESTZuiptwvto\n" + + "CGbOfwuPbqqzzlFqSvX3UiJwhxjSSB3a1EBIOsbhc4grip/wm+fB50S/nTJxkJ14\n" + + "qid40D7HOcIvuz1iQr1QoMNB0oT3nCwMec8mPUX2yOzx1eqr62SZUTCr6FdAmdYI\n" + + "1u4EAeEFhRO0rcPRrpMZqwkXtUfx+pu7OzBS0qmOlfkQ50kbETDXBik4iXi30AGl\n" + + "Ifo792oRo6DFK7ENquTNRqFPfezjrGZfkJrPWulWh28GogWTpOBwfXG8X262QoIp\n" + + "VwZygi7wfj1jh2sXPvWgHjsjjTt7HPAiLI1f6IUl8WCQfPuQkFwCwPv63/rve59v\n" + + "sBaeCEykAxdzMbP1oYSBBtONSAPYW9fsUsJSpuuLvxH252+luk09uQXWd6z4aCDm\n" + + "EXiolhbkzL3mXCpVP6nMjRkm2ERE1yAWgXGT9JON0gcCb3eVqw6wzOYu+Vwq70ND\n" + + "vKYlTMY+9RUx7wLn51UgwUoXQUFBQUFBQUEJYWFhYWFhYWFhYWFhYWFhYWFhYWFh\n" + + "YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYdJMAQZo\n" + + "tUFmYcTQrd5IFriHbF8h/Ov/xKlkW1QOPrZ+ziMQRbwyY4pVwNbZjCNVCHg5QDO4\n" + + "wjF686DfZt83NvVYbJ7QNuENoI4YcFj8nw==\n" + + "=VS1M\n" + + "-----END PGP MESSAGE-----"; + + private static final String PKESK23_PKESK3_SEIP = "-----BEGIN PGP MESSAGE-----\n" + + "\n" + + "wUoXQUFBQUFBQUEJYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFh\n" + + "YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYcHAzAN8L6pN+Tw3sgEL/2D/nw1r\n" + + "Yn0+ykpPrVq4G8r7GAreVodCDxEk2boqbcL7aAhmzn8Lj26qs85Rakr191IicIcY\n" + + "0kgd2tRASDrG4XOIK4qf8JvnwedEv50ycZCdeKoneNA+xznCL7s9YkK9UKDDQdKE\n" + + "95wsDHnPJj1F9sjs8dXqq+tkmVEwq+hXQJnWCNbuBAHhBYUTtK3D0a6TGasJF7VH\n" + + "8fqbuzswUtKpjpX5EOdJGxEw1wYpOIl4t9ABpSH6O/dqEaOgxSuxDarkzUahT33s\n" + + "46xmX5Caz1rpVodvBqIFk6TgcH1xvF9utkKCKVcGcoIu8H49Y4drFz71oB47I407\n" + + "exzwIiyNX+iFJfFgkHz7kJBcAsD7+t/673ufb7AWnghMpAMXczGz9aGEgQbTjUgD\n" + + "2FvX7FLCUqbri78R9udvpbpNPbkF1nes+Ggg5hF4qJYW5My95lwqVT+pzI0ZJthE\n" + + "RNcgFoFxk/STjdIHAm93lasOsMzmLvlcKu9DQ7ymJUzGPvUVMe8C5+dVINJMAQZo\n" + + "tUFmYcTQrd5IFriHbF8h/Ov/xKlkW1QOPrZ+ziMQRbwyY4pVwNbZjCNVCHg5QDO4\n" + + "wjF686DfZt83NvVYbJ7QNuENoI4YcFj8nw==\n" + + "=EhNy\n" + + "-----END PGP MESSAGE-----\n"; + + private static final String PKESK3_SKESK23_SEIP = "-----BEGIN PGP MESSAGE-----\n" + + "\n" + + "wcDMA3wvqk35PDeyAQv/YP+fDWtifT7KSk+tWrgbyvsYCt5Wh0IPESTZuiptwvto\n" + + "CGbOfwuPbqqzzlFqSvX3UiJwhxjSSB3a1EBIOsbhc4grip/wm+fB50S/nTJxkJ14\n" + + "qid40D7HOcIvuz1iQr1QoMNB0oT3nCwMec8mPUX2yOzx1eqr62SZUTCr6FdAmdYI\n" + + "1u4EAeEFhRO0rcPRrpMZqwkXtUfx+pu7OzBS0qmOlfkQ50kbETDXBik4iXi30AGl\n" + + "Ifo792oRo6DFK7ENquTNRqFPfezjrGZfkJrPWulWh28GogWTpOBwfXG8X262QoIp\n" + + "VwZygi7wfj1jh2sXPvWgHjsjjTt7HPAiLI1f6IUl8WCQfPuQkFwCwPv63/rve59v\n" + + "sBaeCEykAxdzMbP1oYSBBtONSAPYW9fsUsJSpuuLvxH252+luk09uQXWd6z4aCDm\n" + + "EXiolhbkzL3mXCpVP6nMjRkm2ERE1yAWgXGT9JON0gcCb3eVqw6wzOYu+Vwq70ND\n" + + "vKYlTMY+9RUx7wLn51Ugw00XCQMINQp7MFzAc6T/YWFhYWFhYWFhYWFhYWFhYWFh\n" + + "YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYdJM\n" + + "AQZotUFmYcTQrd5IFriHbF8h/Ov/xKlkW1QOPrZ+ziMQRbwyY4pVwNbZjCNVCHg5\n" + + "QDO4wjF686DfZt83NvVYbJ7QNuENoI4YcFj8nw==\n" + + "=pvWj\n" + + "-----END PGP MESSAGE-----\n"; + + private static final String SKESK23_PKESK3_SEIP = "-----BEGIN PGP MESSAGE-----\n" + + "\n" + + "w00XCQMINQp7MFzAc6T/YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFh\n" + + "YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYcHAzAN8L6pN+Tw3sgEL/2D/\n" + + "nw1rYn0+ykpPrVq4G8r7GAreVodCDxEk2boqbcL7aAhmzn8Lj26qs85Rakr191Ii\n" + + "cIcY0kgd2tRASDrG4XOIK4qf8JvnwedEv50ycZCdeKoneNA+xznCL7s9YkK9UKDD\n" + + "QdKE95wsDHnPJj1F9sjs8dXqq+tkmVEwq+hXQJnWCNbuBAHhBYUTtK3D0a6TGasJ\n" + + "F7VH8fqbuzswUtKpjpX5EOdJGxEw1wYpOIl4t9ABpSH6O/dqEaOgxSuxDarkzUah\n" + + "T33s46xmX5Caz1rpVodvBqIFk6TgcH1xvF9utkKCKVcGcoIu8H49Y4drFz71oB47\n" + + "I407exzwIiyNX+iFJfFgkHz7kJBcAsD7+t/673ufb7AWnghMpAMXczGz9aGEgQbT\n" + + "jUgD2FvX7FLCUqbri78R9udvpbpNPbkF1nes+Ggg5hF4qJYW5My95lwqVT+pzI0Z\n" + + "JthERNcgFoFxk/STjdIHAm93lasOsMzmLvlcKu9DQ7ymJUzGPvUVMe8C5+dVINJM\n" + + "AQZotUFmYcTQrd5IFriHbF8h/Ov/xKlkW1QOPrZ+ziMQRbwyY4pVwNbZjCNVCHg5\n" + + "QDO4wjF686DfZt83NvVYbJ7QNuENoI4YcFj8nw==\n" + + "=STOd\n" + + "-----END PGP MESSAGE-----\n"; + + private static final String PKESK3_SKESK4wS2K23_SEIP = "-----BEGIN PGP MESSAGE-----\n" + + "\n" + + "wcDMA3wvqk35PDeyAQv/YP+fDWtifT7KSk+tWrgbyvsYCt5Wh0IPESTZuiptwvto\n" + + "CGbOfwuPbqqzzlFqSvX3UiJwhxjSSB3a1EBIOsbhc4grip/wm+fB50S/nTJxkJ14\n" + + "qid40D7HOcIvuz1iQr1QoMNB0oT3nCwMec8mPUX2yOzx1eqr62SZUTCr6FdAmdYI\n" + + "1u4EAeEFhRO0rcPRrpMZqwkXtUfx+pu7OzBS0qmOlfkQ50kbETDXBik4iXi30AGl\n" + + "Ifo792oRo6DFK7ENquTNRqFPfezjrGZfkJrPWulWh28GogWTpOBwfXG8X262QoIp\n" + + "VwZygi7wfj1jh2sXPvWgHjsjjTt7HPAiLI1f6IUl8WCQfPuQkFwCwPv63/rve59v\n" + + "sBaeCEykAxdzMbP1oYSBBtONSAPYW9fsUsJSpuuLvxH252+luk09uQXWd6z4aCDm\n" + + "EXiolhbkzL3mXCpVP6nMjRkm2ERE1yAWgXGT9JON0gcCb3eVqw6wzOYu+Vwq70ND\n" + + "vKYlTMY+9RUx7wLn51Ugw1AECRcIYWFhYWFhYWFBQUFBYWFhYWFhYWFhYWFhYWFh\n" + + "YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFh\n" + + "YdJMAQZotUFmYcTQrd5IFriHbF8h/Ov/xKlkW1QOPrZ+ziMQRbwyY4pVwNbZjCNV\n" + + "CHg5QDO4wjF686DfZt83NvVYbJ7QNuENoI4YcFj8nw==\n" + + "=/uxY\n" + + "-----END PGP MESSAGE-----\n"; + + private static final String SKESK4wS2K23_PKESK3_SEIP = "-----BEGIN PGP MESSAGE-----\n" + + "\n" + + "w1AECRcIYWFhYWFhYWFBQUFBYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFh\n" + + "YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYcHAzAN8L6pN+Tw3sgEL\n" + + "/2D/nw1rYn0+ykpPrVq4G8r7GAreVodCDxEk2boqbcL7aAhmzn8Lj26qs85Rakr1\n" + + "91IicIcY0kgd2tRASDrG4XOIK4qf8JvnwedEv50ycZCdeKoneNA+xznCL7s9YkK9\n" + + "UKDDQdKE95wsDHnPJj1F9sjs8dXqq+tkmVEwq+hXQJnWCNbuBAHhBYUTtK3D0a6T\n" + + "GasJF7VH8fqbuzswUtKpjpX5EOdJGxEw1wYpOIl4t9ABpSH6O/dqEaOgxSuxDark\n" + + "zUahT33s46xmX5Caz1rpVodvBqIFk6TgcH1xvF9utkKCKVcGcoIu8H49Y4drFz71\n" + + "oB47I407exzwIiyNX+iFJfFgkHz7kJBcAsD7+t/673ufb7AWnghMpAMXczGz9aGE\n" + + "gQbTjUgD2FvX7FLCUqbri78R9udvpbpNPbkF1nes+Ggg5hF4qJYW5My95lwqVT+p\n" + + "zI0ZJthERNcgFoFxk/STjdIHAm93lasOsMzmLvlcKu9DQ7ymJUzGPvUVMe8C5+dV\n" + + "INJMAQZotUFmYcTQrd5IFriHbF8h/Ov/xKlkW1QOPrZ+ziMQRbwyY4pVwNbZjCNV\n" + + "CHg5QDO4wjF686DfZt83NvVYbJ7QNuENoI4YcFj8nw==\n" + + "=cIwV\n" + + "-----END PGP MESSAGE-----\n"; + + private static final String PKESK3_SEIP_OPS3_LIT_SIG4 = "-----BEGIN PGP MESSAGE-----\n" + + "\n" + + "wcDMA3wvqk35PDeyAQv+IhkCMhbdRcMnIPZNPGU6OK1Jk5xuRdIEIBsvv7b8jmAr\n" + + "9IwjfnV/RDMtH+xR/T9K7qJGGFYnhLY5w0CmYHQcDKpcBqk0Dw6l/eKCNhgRXKAk\n" + + "gfaKL1Utt1Pw0nz0mOwHyPEN/pGc0xlVhsjVkRvIOsKcpfuc1EpSZMFgDcBQDhe/\n" + + "jAsR/MvRugkW8xLpyQyfeGLJUOEYVrkpam3rLKB1KywAgBmpr9WDwfYITW/VE9k5\n" + + "cKIOPMDJFU+u9lzBx6nSS4JRBuCO2mhR4gjcRaGPWiiz+0qZfS+AXYV/MAU5OwK/\n" + + "6nhX97zwS4r1Avztjh4taBhLVsY4pw6PuLtACJNwPrev63Yc+a4hJJ4pm1AnHV58\n" + + "Y1pZKQL8vA61+/tbhFQ18vbJ8E1NOka/euFLQu9Mg58jhpcscqMouyr3JFMwgH2Y\n" + + "eFuRJncJAKotXxlfnF37qz5LG3bamACXWZSObjp9d4quIAoCDUteZlDWQ1xq5R4y\n" + + "QYXtk9ZuHsHsmY9A0CiI0sGYAUBtLubJ5qhLLr/GqKAmy8jTSA3MjgtrB55NWj9J\n" + + "bjgFNsd1BNGklgnwhtmApHJWY8skAAQkJj0rXj/aOMc734ypiEWDiU1quRbEeRLR\n" + + "kDvBNUXx2j2rVF+MmQS/sm5Yk/op+4lH/Wounsci3qWH76GaNZoIlvNE3mdFoVTe\n" + + "cRh4W2Em8uAUH4bKwazltRJUhZmXvuGfUnQCmolJTpyPl4DaQQgzdBXLTRcPxwdU\n" + + "30e7HnxZWESKx1LnGxp3Oan1k1lXyHwvnEk26EXhve+dhsQ6YsKgvtSLNFqGsfKe\n" + + "MVOq37cpOGFQsYStWHZd0tcHjIjWmAeZ8kH+ZzR9tgYKxxjimsxafLS/lo415SkC\n" + + "LnOCz6hywI7CufSUcXUlHGJuobZ5HDJcygsQhQmNVLDmKh4xUJDZrORS0ciMy2kc\n" + + "XAnxCDYbltVQktc/F/Gl1lTx0UNmV5d9G0utVmxxGbXna3BLkB+6qMuux/ngC+cI\n" + + "+0GjeuXDVzUqhKDDC3Sq4T2nwix4CjKvawnHC+vpHzRmdZSkXz3nrSdhJS5JnOap\n" + + "I4CdyYkP3jQs5P03dkv5RZAoqNPkeftwadu0cjZj8HEC8bVSd7YCS/0Gbgvp/eK3\n" + + "aOXZAWhELyQON4bbXiWzMTcO2AA5soBmP2QnBNdq7NxbEkBec8aAXZiyXn17JYit\n" + + "HRtJ7beptXjN0y2bEhOvsBHbDpjGO22fZfQ0aywP2k4/XanDf4WJolEFgLj2Qp8F\n" + + "pw1Olc2UhApxhAqjkme09hggli3wUhthQrpBlmntfbvBcmmO+p/jV8Zn\n" + + "=Roko\n" + + "-----END PGP MESSAGE-----\n"; + + private static final String PKESK3_SEIP_OPS23_LIT_SIG23 = "-----BEGIN PGP MESSAGE-----\n" + + "\n" + + "wcDMA3wvqk35PDeyAQv/byYGOehnfgZu/HJSSDEQhYE27lh5j+oQPktYBxsTI3Ii\n" + + "AJ0pyJggtUM3c/e6z4HioAs4oQiaH1eoGBIl7noROhHJgT6E/oKdHJHmQndo0gT7\n" + + "xzSHKrEZYEqT45LD6jT5teOiwGX/B7as6jaQOT+Nh+M0ZqpPrBdWDeUoY/I/lx9j\n" + + "IcXQUhKuqJwZ16xsnv0JJ80rdp5qv0g2NHT1hK1JEONyT2fefov3EXaSpQZHRBEi\n" + + "XfwToHcJrgemFoGwZBQhXsPWBgKH92aW6r7ZJZMQ4BE2SwqEw+cbaaqwfFRJ9puj\n" + + "ZUBi2JGwnYImZyD7jYverkjH05vI7d5qQDhCT6GPD8Q0WKfY225LFTj/zzbC23lp\n" + + "VLlbT2Ap8ZFOESpM+crOUaOguBhnTOF05s1eXhQYxIKzJTW8UVrzbwI8ut3BnEDj\n" + + "0aDqUR+QDYgYmz8hnEjanHk2McvfaNdV6uOPZNph2RuPzhZeoNcz1PLy+XPFJsQT\n" + + "EdqURJ2D35qmrBvq6klN0sGYAXEtW4hxMaKhH7/SIk2m3kUAChjtzr4XRcE2i8h3\n" + + "9uD6CRWxokArfRVAp5RpXt3ywoYrl1Mp5prVwWcrTzYVPZwwe/bYAFTIfavi0Ezb\n" + + "A/ah2e1EYCTWxLY0Klzil2Xw9/Dc/JPTRqzWJxIn0AU4DVfYNwlH3QimDUDKOKbu\n" + + "bw4bLEEBKRr5BcNMA2rJOw/n3AmKxQcdSFJh3ZNtDPWzRwflIzE2qB686hpeOs34\n" + + "T2iJcfr9W9rKcYI7+WYcZA3fWokaWUfXdWrPMXBVJuPdGezGLSfe/OM4kw/8s2vR\n" + + "Bk88WZ1ZiFXE2CRaHP80fHFpxAZioWTuC5UGhF7NgZ7Q1E85GaGVe1fQeqmeX3mo\n" + + "gwAWwq9WFhPQQPwdrDz+1h/pzD0RVW7D+zfWdF//vesc4z1Bpi5prbMdpVdmyvO9\n" + + "8Rcc42GtmhtYSB9SPjzPuN8PrWvD3AKgw5vQro6oiNh0TGmj5Se4lXCfRfCl4tak\n" + + "cmvdi+1wAvn5OFdxYKKHNvjavwj2SY70nx0ACasBpbMEwoQ0StZmOxCaofkgvEwN\n" + + "t8jMq/MVrIfunhMjx0/GDpBZBb8kze8zrvWxlbTnoIfh2yVEqmTZWue9HX5Mnh8P\n" + + "wexxNrPaafTjA3nUgXXzlItJ/Wa43pYw2sgcBPlF/jRKSFiD+pDLysqwpH0ANsJ3\n" + + "G7t8Qavq1DlrHFgV5jhfR1tjA5ohjxx7yzceQBvZUFxKM1WEWR+9dRb3bpfZJr0g\n" + + "qgO36bpCeCEej+ubXpXXTN28LQLXjQHlE2o1NGLoGl+G72tXOTx30kPS\n" + + "=wUZn\n" + + "-----END PGP MESSAGE-----\n"; + + private static final String PKESK3_SEIP_OPS23_OPS3_LIT_SIG4_SIG23 = "-----BEGIN PGP MESSAGE-----\n" + + "\n" + + "wcDMA3wvqk35PDeyAQwAmftqzbRiMRk7YbpJO7uqtWQIq2uZj7nYYOVpQgR777Zb\n" + + "QT1J3pbPoCceWFSuHH/D0IeP84T95O+A630dtj3ZWhh4EGnBIKm13R6bEA4jKtcx\n" + + "rK3HZyHo85N5TFn/gLOqCc2v/0cxUoygBqFpHFwBG/e4uuQoxgD4RsBGFrdYlyK7\n" + + "MMkZ/fgaKhawTCVQ2dtBcp6WvKX/8u6FXSh35zFJJTM30LE7BFDgWEyGPLv0bWnl\n" + + "37oCo8zcK+SIK4JooNm1ZxIowWTOxUz1pWPtmtGXA8XoGJtggXt+HpVxNE8bDCgV\n" + + "f717+R0HMwQKP4F50Wq3Js9ZJrPjRvXLeiZe6nvqn5AQCOsG8osIIg2YElyW/uIe\n" + + "C38OxaZbql297cYzzdEZtRYCTTY7j99pG8ZD1nNdd5IPm027dfPl+JJ5nzn/u3iT\n" + + "+FKxgArZ9cYkeEpNB+BfWoWyNfbA830s3Y+G5wt3s4cmH1z8JvDeepUYhyqvA/6M\n" + + "RInjwCUwSQCAuI+QhnPi0sOlAYOwkgN8RyqOP4vR4Kd1rJfm7rg2h40ag7tJtu5y\n" + + "YXa4sU9DLpspTYvYgzRtsNbYHrH+LFkQlpG+PFnJAI6Qn+Q1zZsZ2HbbK7GOZFug\n" + + "0wCg5E3DJQxBNlOx7h7xacpGb2QsmBTLUPvWFiYoq8He5XOx1MleLxO+E5l40kT4\n" + + "ZE8RxfRyVreQdp3rZ6RMiIfnM8VljMxteaLmaIzqsLTlvKlf1w2DBRL9reI2oCP6\n" + + "BukX8zba16ITsLELnuQ5L8EmQwcH8Yj8Mg37foyFHa2fIvZRg+0tz3b5nJKteWQn\n" + + "u/qR/RGNAXT1D/YBQ+Tgnq6qIUd4Da/XEXAi8R6FKprc2yqCNzMSA1wolaq2DUGJ\n" + + "ASyRN7uQJpVc1DlxTRTMQLpuoxljQtc6dmn/HKr7DF8jUKcM8cRk6PCqWd6PRYPq\n" + + "WJTiHh2FoyDaR5+HuSbRCOr7i9jZXh/TctM70itLIvQlw/x9WEm2ZxwS7+0mHMvP\n" + + "h9U4Wi70mfR0WllDNpWm5ZEeksoUF7aCQ2lVIQH8E6YGmWUSCYUgjgiIsfSqn9kh\n" + + "tG8WGCrM1sPSIzQG70d1fuirRg5H7oeVRTPzpYN/cSXqRULk31Z9RneXwgZZZgPB\n" + + "Q1hE3oJmP4LJEfRhL4P7TL2Xp+1Kvius53my2zKnVXoBNlAUHSdidXsd+xVaOEkE\n" + + "cNyhLg4cZmlyuz5Ew/NHPAD70Cd9qXQraOf3dqZ77yhG0y/FCwXxnnfW1FnTLe14\n" + + "3RWuNAFhbuNuYrXn0Zq+SFz3UnNNKMoNejwDcvkxxZ92KQXJcB7zCRnEehjBz/At\n" + + "iNgsVfiOVRxzzp12iV+ljtM8A3KJHnnBQypPIeq4yKsXxtumVhryAc5k2neRZkvc\n" + + "Wo1x3T/EY0SSlFFSYsiyDgbaj0SguiVNTrJbLQd62a1S4ZCYB5k2hlzm22eIKHIa\n" + + "lb+sYaTGbSkxVH2xMvjxgO4dx9YvTlH6rsTIktmhvYxnF27Y6Bfhp+x8I3RoYPRC\n" + + "ImMgllybYE9AOHLI5uogvoe133OfHAmHVm36qx+S24r8YTMdZ6iJKLCd0Hav6aS9\n" + + "b4ptBiKQQQR2mtxaQNyBVEjfbpt2/ATnzRg5D/TAJATvhoeByWRYNP21iATnWU5c\n" + + "H3uK3dNDLnZAbaEf2XfqEG2fcw/Bn9mbXUodEay+EQl0Z11kWKOBSwMyGwSxdMhw\n" + + "S+9tTnfFZ73B/fyD41p3Ft02cUJcD2yW/j3+5JLOqZJJlTEhtAFvixkhcfR7VJTl\n" + + "arZfECPXOOMbiBxQmFA4+AZfP+9bMFPz9/guZTkIWsjKO4JI6ge6ayl6Eel2Qsbo\n" + + "MzsYA6m9h4a0VQPmHf1Itg5kiEpecG4rEqzJC2ov4mTiD4kVlPhUj6Je+VU3mEgT\n" + + "geMf/8JkD6+IParbR7iaEQF2wPgrR/VcBX/5Y8AI4mW8eiTCybtPt9z4X6w326Uy\n" + + "UkaqeswhQmd2sODDqxxrdjVmYQEWqVKIRRBLsR5fvDiqyiVFbPEO\n" + + "=qFHk\n" + + "-----END PGP MESSAGE-----\n"; + + private static final String PKESK3_SEIP_OPS3_OPS23_LIT_SIG23_SIG4 = "-----BEGIN PGP MESSAGE-----\n" + + "\n" + + "wcDMA3wvqk35PDeyAQv+JdNtgn26/Q4yXJ9egCmJ1/3SaeSB+y93jz+fmabMvc97\n" + + "KbI1HN9RoG23UxDoQED+jGcbbrw+7ho73U3uRC4NtE4D4SZZMQZZXTadI0MEu4eN\n" + + "yGgyFmZLy2Fek5N33m4PShvKqbeDnubmxv/lPlpz//KuxbVNPL+vtiVxZuI6vusB\n" + + "q3T0Br0kH7OqCizCIKzl85d9hYAKXDaCaGw/0VXmFs0HgjBSB5RRHKt3GpVt83dI\n" + + "b7vyyrzo6D0bwr+9nYC9Q+rku0lPNJutdSBRv9Xoc8eB7ud+0x9Ybj61C91gjvJp\n" + + "NkeuqcCYOZw+iFckDR8+GUM4T5PO3dwxEVQUUeO99EKuCH2S/PqgrH8JFiRcCvED\n" + + "SsngpAL3IfkoKCuAO0gpK4ebDKQ8R46ho/ER1UApT9A8lNbzLYIwCQ8fcK53AICe\n" + + "XWDTGb1uqqkt0vFPxsKA0R6Wyk6gA8j8ta6zpgTpwGOyRbMKo8QW08mw+2UROCPj\n" + + "/cdtV1Z6Iv0GrbKwrwB20sOlAYDKbSloZBhkwugYMqYWQfzPcM8S+sCX/+Kv/O79\n" + + "oWucvbSAiUv0JlD6zqdNcileqJKCiiAVaCUZhLpZcgVWpLqfJuFFnHBOo98ARIHe\n" + + "D5CzXn2sx+0ZlFW0fJk8Z2ZWXK9rKAleqsGB4dIA3WoC4UAFFjBqXG/4pa/H0He2\n" + + "G8R3Q+wFqEaXYgm2Znq/+UxPGjAJLH7EUrwfBvK17eByT+bLqyZpKHhuZJXy/rw1\n" + + "n+pCC1fedDWdxKj7+1Xw8cVlAWYCp2314DQjfI8BzFw0JWq8MU7hwGDQkJgfI3qH\n" + + "RlBhIgE2iJAOQi4YaSgC9QaAxL4Uw3uo9+kwUGt87j+M4d7ALQ1XXLtCim6P36jP\n" + + "kjOoAvfgwZ1NZEbzo/YS9/NK9KVXrhfgmkWSPjLaqJeur2Av9IkDuQmFF+EVxjRo\n" + + "eQzZHk8RHOj03jjTZH/QHNiiUDfr2cMDj8Hi+r5pCvS/T67gymyE6VHQJSYS9dXP\n" + + "0EMoe4jaG+aOdbAi3OPtKETSvtsKLflMR4k/RxRXN6lsV238wVna+w6nYZxqMqd4\n" + + "fNnL5YUHZOEt2qxVguOCEZDANHoR0RFVXM6yBF36Ivwjhg5a1aujyHv150KwDDsc\n" + + "YMI4O2pTcmhDX1aiV2X35EyLWJbSovbNj/IveMKa0q/xOXe4V8INX9Xv4sxm0mqY\n" + + "RR8CY1E3cYG2g6Uhc4WkirSvXoN/IRq1MrYmcCHQrEDDBL5h/8/TIn/TAOZqBrZ/\n" + + "gF1gfFW4ZgEcPZUeUmErHxLvdVqC+WK7/5qE86PXGo9yD9/Xxv5U7i8BbxgknUlD\n" + + "SyRmBfzkRJudTHvH9wnk9KA5hPHqXk6ZkrBo4ugaiwUa/EejvkiHW7KRWijH57nL\n" + + "JDzP13FU16gPuhPFTXP2zLvLeMSpVmkv2B9Mzvnrg+836B+hY0elAy5U/3D1/wxG\n" + + "IjfDTsAcUQ8yULCRj6iZrx1SZc0HJDe4mjvqVqg0VOSpxGHfRNcte+zh6p8Fqqly\n" + + "2O72vb49uEr54ZeL33j/ggCXWvMgdK7EtIirmzcwFsmamy89QSl1VzZzh5338n7f\n" + + "9SOd67xL8BVSh8lee8ByuBiZryLbIuy1d8stndbbxLi+W7+Y/W08g3QyE+NwcpKd\n" + + "/zTVViCZolgs4Ol73WEe6A131u+AMlJWXYD5tai+RmQOFugvCVX+QhezK1v3YrMH\n" + + "KlIfFsh4Cq+JIo2jMMoVjLBK662kU24w8eaEagdIjBgd1XlEBgKUR/f754BOfoKi\n" + + "JX2ySeHdQCCn/yc753X1TH3FNEThmJPHJG0ESkpIxqoTKdL3Ut+8BFlhWYwxCc8r\n" + + "R8m9ixq0cQBXrNVaJsFVKqI9H4SJMc8ySGe8HYwJV2hhK9HbuhAfrKiJoUmoQHvD\n" + + "jL9Y6H3ejK5YmqQ/zXoiepRfAklN3q+ByqhRMjZfDMuk0fcMaPy9RFoo1FqIPyqw\n" + + "alekNaR/K4albyRcMoYxBhn3QFHf7VuaPuaxhg1ri3YfrWykv3RA\n" + + "=jGpv\n" + + "-----END PGP MESSAGE-----\n"; + + private final PGPSecretKeyRing key; + private final PGPPublicKeyRing cert; + + public UnsupportedPacketVersionsTest() throws IOException { + key = PGPainless.readKeyRing().secretKeyRing(KEY); + cert = PGPainless.extractCertificate(key); + } + + @Test + public void pkesk3_pkesk23_seip() throws PGPException, IOException { + decryptAndCompare(PKESK3_PKESK23_SEIP, "Encrypted using SEIP + MDC."); + } + + @Test + public void pkesk23_pkesk_seip() throws PGPException, IOException { + decryptAndCompare(PKESK23_PKESK3_SEIP, "Encrypted using SEIP + MDC."); + } + + @Test + public void pkesk3_skesk23_seip() throws PGPException, IOException { + decryptAndCompare(PKESK3_SKESK23_SEIP, "Encrypted using SEIP + MDC."); + } + + @Test + public void skesk23_pkesk3_seip() throws PGPException, IOException { + decryptAndCompare(SKESK23_PKESK3_SEIP, "Encrypted using SEIP + MDC."); + } + + @Test + @Disabled("Enable once https://github.com/bcgit/bc-java/pull/1268 is available") + public void pkesk3_skesk4Ws2k23_seip() throws PGPException, IOException { + decryptAndCompare(PKESK3_SKESK4wS2K23_SEIP, "Encrypted using SEIP + MDC."); + } + + @Test + @Disabled("Enable once https://github.com/bcgit/bc-java/pull/1268 is available") + public void skesk4Ws2k23_pkesk3_seip() throws PGPException, IOException { + decryptAndCompare(SKESK4wS2K23_PKESK3_SEIP, "Encrypted using SEIP + MDC."); + } + + @Test + public void pkesk3_seip_ops3_lit_sig4() throws PGPException, IOException { + decryptAndCompare(PKESK3_SEIP_OPS3_LIT_SIG4, "Encrypted, signed message."); + } + + @Test + public void pkesk3_seip_ops23_lit_sig23() throws PGPException, IOException { + decryptAndCompare(PKESK3_SEIP_OPS23_LIT_SIG23, "Encrypted, signed message."); + } + + @Test + public void pkesk3_seip_ops23_ops3_lit_sig4_sig23() throws PGPException, IOException { + decryptAndCompare(PKESK3_SEIP_OPS23_OPS3_LIT_SIG4_SIG23, "Encrypted, signed message."); + } + + @Test + public void pkesk3_seip_ops3_ops23_lit_sig23_sig4() throws PGPException, IOException { + decryptAndCompare(PKESK3_SEIP_OPS3_OPS23_LIT_SIG23_SIG4, "Encrypted, signed message."); + } + + public void decryptAndCompare(String msg, String plain) throws IOException, PGPException { + // noinspection CharsetObjectCanBeUsed + ByteArrayInputStream inputStream = new ByteArrayInputStream(msg.getBytes(Charset.forName("UTF8"))); + DecryptionStream decryptionStream = PGPainless.decryptAndOrVerify() + .onInputStream(inputStream) + .withOptions(ConsumerOptions.get() + .addDecryptionKey(key) + .addVerificationCert(cert)); + + ByteArrayOutputStream out = new ByteArrayOutputStream(); + Streams.pipeAll(decryptionStream, out); + decryptionStream.close(); + + assertEquals(plain, out.toString()); + } +} From 4aaecb7df865eac6c39bc39ff66f9378a7c9812c Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 26 Oct 2022 18:22:50 +0200 Subject: [PATCH 55/80] Remove superfluous states --- .../syntax_check/PDA.java | 26 +++---------------- .../syntax_check/StackAlphabet.java | 4 --- 2 files changed, 3 insertions(+), 27 deletions(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/PDA.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/PDA.java index 0d6ba28c..c77b50cc 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/PDA.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/PDA.java @@ -67,7 +67,7 @@ public class PDA { case Signature: if (stackItem == ops) { - return CorrespondingSignature; + return LiteralMessage; } else { throw new MalformedOpenPgpMessageException(this, input, stackItem); } @@ -96,7 +96,7 @@ public class PDA { switch (input) { case Signature: if (stackItem == ops) { - return CorrespondingSignature; + return CompressedMessage; } else { throw new MalformedOpenPgpMessageException(this, input, stackItem); } @@ -125,7 +125,7 @@ public class PDA { switch (input) { case Signature: if (stackItem == ops) { - return CorrespondingSignature; + return EncryptedMessage; } else { throw new MalformedOpenPgpMessageException(this, input, stackItem); } @@ -147,26 +147,6 @@ public class PDA { } }, - CorrespondingSignature { - @Override - State transition(InputAlphabet input, PDA automaton) throws MalformedOpenPgpMessageException { - StackAlphabet stackItem = automaton.popStack(); - if (input == InputAlphabet.EndOfSequence) { - if (stackItem == terminus && automaton.stack.isEmpty()) { - return Valid; - } else { - // premature end of stream - throw new MalformedOpenPgpMessageException(this, input, stackItem); - } - } else if (input == InputAlphabet.Signature) { - if (stackItem == ops) { - return CorrespondingSignature; - } - } - throw new MalformedOpenPgpMessageException(this, input, stackItem); - } - }, - Valid { @Override State transition(InputAlphabet input, PDA automaton) throws MalformedOpenPgpMessageException { diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/StackAlphabet.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/StackAlphabet.java index 6030fbc8..a8a2a213 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/StackAlphabet.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/StackAlphabet.java @@ -13,10 +13,6 @@ public enum StackAlphabet { * OnePassSignature (in case of BC this represents a OnePassSignatureList). */ ops, - /** - * ESK. Not used, as BC combines encrypted data with their encrypted session keys. - */ - esk, /** * Special symbol representing the end of the message. */ From 0feafbf7ed4c3d000ba6b808e8075da040f2935a Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 26 Oct 2022 18:24:19 +0200 Subject: [PATCH 56/80] Remove debugging fields --- .../pgpainless/decryption_verification/syntax_check/PDA.java | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/PDA.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/PDA.java index c77b50cc..40021734 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/PDA.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/PDA.java @@ -16,7 +16,6 @@ import static org.pgpainless.decryption_verification.syntax_check.StackAlphabet. public class PDA { - private static int ID = 0; private static final Logger LOGGER = LoggerFactory.getLogger(PDA.class); /** @@ -171,13 +170,11 @@ public class PDA { private final Stack stack = new Stack<>(); private State state; - private int id; public PDA() { state = State.OpenPgpMessage; stack.push(terminus); stack.push(msg); - this.id = ID++; } public void next(InputAlphabet input) throws MalformedOpenPgpMessageException { @@ -240,6 +237,6 @@ public class PDA { @Override public String toString() { - return "PDA " + id + ": State: " + state + " Stack: " + stack; + return "State: " + state + " Stack: " + stack; } } From c4e937c0f936b6a7b08e507426620655c0531e1d Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 26 Oct 2022 18:39:20 +0200 Subject: [PATCH 57/80] Improve syntax error reporting --- .../decryption_verification/syntax_check/PDA.java | 11 +++++++++-- .../exception/MalformedOpenPgpMessageException.java | 6 +++++- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/PDA.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/PDA.java index 40021734..550cb855 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/PDA.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/PDA.java @@ -8,6 +8,9 @@ import org.pgpainless.exception.MalformedOpenPgpMessageException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; import java.util.Stack; import static org.pgpainless.decryption_verification.syntax_check.StackAlphabet.msg; @@ -169,6 +172,7 @@ public class PDA { } private final Stack stack = new Stack<>(); + private final List inputs = new ArrayList<>(); // keep track of inputs for debugging / error reporting private State state; public PDA() { @@ -180,9 +184,12 @@ public class PDA { public void next(InputAlphabet input) throws MalformedOpenPgpMessageException { try { state = state.transition(input, this); + inputs.add(input); } catch (MalformedOpenPgpMessageException e) { - LOGGER.debug("Unexpected Packet or Token '" + input + "' encountered. Message is malformed.", e); - throw e; + MalformedOpenPgpMessageException wrapped = new MalformedOpenPgpMessageException("Malformed message: After reading stream " + Arrays.toString(inputs.toArray()) + + ", token '" + input + "' is unexpected and illegal.", e); + LOGGER.debug("Invalid input '" + input + "'", wrapped); + throw wrapped; } } diff --git a/pgpainless-core/src/main/java/org/pgpainless/exception/MalformedOpenPgpMessageException.java b/pgpainless-core/src/main/java/org/pgpainless/exception/MalformedOpenPgpMessageException.java index cbe78c05..aa158746 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/exception/MalformedOpenPgpMessageException.java +++ b/pgpainless-core/src/main/java/org/pgpainless/exception/MalformedOpenPgpMessageException.java @@ -21,6 +21,10 @@ public class MalformedOpenPgpMessageException extends RuntimeException { } public MalformedOpenPgpMessageException(PDA.State state, InputAlphabet input, StackAlphabet stackItem) { - this("Invalid input: There is no legal transition from state '" + state + "' for input '" + input + "' when '" + stackItem + "' is on top of the stack."); + this("There is no legal transition from state '" + state + "' for input '" + input + "' when '" + stackItem + "' is on top of the stack."); + } + + public MalformedOpenPgpMessageException(String s, MalformedOpenPgpMessageException e) { + super(s, e); } } From a9f67c508fba6b0e87010858d38b90da7ec3e20f Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 27 Oct 2022 11:56:10 +0200 Subject: [PATCH 58/80] Separate out syntax logic --- .../syntax_check/OpenPgpMessageSyntax.java | 119 +++++++++++++ .../syntax_check/PDA.java | 165 ++---------------- .../syntax_check/State.java | 16 ++ .../syntax_check/Syntax.java | 13 ++ .../syntax_check/Transition.java | 28 +++ .../MalformedOpenPgpMessageException.java | 4 +- 6 files changed, 189 insertions(+), 156 deletions(-) create mode 100644 pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/OpenPgpMessageSyntax.java create mode 100644 pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/State.java create mode 100644 pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/Syntax.java create mode 100644 pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/Transition.java diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/OpenPgpMessageSyntax.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/OpenPgpMessageSyntax.java new file mode 100644 index 00000000..ab45a896 --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/OpenPgpMessageSyntax.java @@ -0,0 +1,119 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.decryption_verification.syntax_check; + +import org.pgpainless.exception.MalformedOpenPgpMessageException; + +public class OpenPgpMessageSyntax implements Syntax { + + @Override + public Transition transition(State from, InputAlphabet input, StackAlphabet stackItem) + throws MalformedOpenPgpMessageException { + switch (from) { + case OpenPgpMessage: + return fromOpenPgpMessage(input, stackItem); + case LiteralMessage: + return fromLiteralMessage(input, stackItem); + case CompressedMessage: + return fromCompressedMessage(input, stackItem); + case EncryptedMessage: + return fromEncryptedMessage(input, stackItem); + case Valid: + return fromValid(input, stackItem); + } + + throw new MalformedOpenPgpMessageException(from, input, stackItem); + } + + Transition fromOpenPgpMessage(InputAlphabet input, StackAlphabet stackItem) + throws MalformedOpenPgpMessageException { + if (stackItem != StackAlphabet.msg) { + throw new MalformedOpenPgpMessageException(State.OpenPgpMessage, input, stackItem); + } + + switch (input) { + case LiteralData: + return new Transition(State.LiteralMessage); + + case Signature: + return new Transition(State.OpenPgpMessage, StackAlphabet.msg); + + case OnePassSignature: + return new Transition(State.OpenPgpMessage, StackAlphabet.ops, StackAlphabet.msg); + + case CompressedData: + return new Transition(State.CompressedMessage); + + case EncryptedData: + return new Transition(State.EncryptedMessage); + + case EndOfSequence: + default: + throw new MalformedOpenPgpMessageException(State.OpenPgpMessage, input, stackItem); + } + } + + Transition fromLiteralMessage(InputAlphabet input, StackAlphabet stackItem) + throws MalformedOpenPgpMessageException { + switch (input) { + case Signature: + if (stackItem == StackAlphabet.ops) { + return new Transition(State.LiteralMessage); + } + break; + + case EndOfSequence: + if (stackItem == StackAlphabet.terminus) { + return new Transition(State.Valid); + } + break; + } + + throw new MalformedOpenPgpMessageException(State.LiteralMessage, input, stackItem); + } + + Transition fromCompressedMessage(InputAlphabet input, StackAlphabet stackItem) + throws MalformedOpenPgpMessageException { + switch (input) { + case Signature: + if (stackItem == StackAlphabet.ops) { + return new Transition(State.CompressedMessage); + } + break; + + case EndOfSequence: + if (stackItem == StackAlphabet.terminus) { + return new Transition(State.Valid); + } + break; + } + + throw new MalformedOpenPgpMessageException(State.CompressedMessage, input, stackItem); + } + + Transition fromEncryptedMessage(InputAlphabet input, StackAlphabet stackItem) + throws MalformedOpenPgpMessageException { + switch (input) { + case Signature: + if (stackItem == StackAlphabet.ops) { + return new Transition(State.EncryptedMessage); + } + break; + + case EndOfSequence: + if (stackItem == StackAlphabet.terminus) { + return new Transition(State.Valid); + } + break; + } + + throw new MalformedOpenPgpMessageException(State.EncryptedMessage, input, stackItem); + } + + Transition fromValid(InputAlphabet input, StackAlphabet stackItem) + throws MalformedOpenPgpMessageException { + throw new MalformedOpenPgpMessageException(State.Valid, input, stackItem); + } +} diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/PDA.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/PDA.java index 550cb855..94c1739e 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/PDA.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/PDA.java @@ -14,177 +14,31 @@ import java.util.List; import java.util.Stack; import static org.pgpainless.decryption_verification.syntax_check.StackAlphabet.msg; -import static org.pgpainless.decryption_verification.syntax_check.StackAlphabet.ops; import static org.pgpainless.decryption_verification.syntax_check.StackAlphabet.terminus; public class PDA { private static final Logger LOGGER = LoggerFactory.getLogger(PDA.class); - /** - * Set of states of the automaton. - * Each state defines its valid transitions in their {@link State#transition(InputAlphabet, PDA)} method. - */ - public enum State { - - OpenPgpMessage { - @Override - State transition(InputAlphabet input, PDA automaton) throws MalformedOpenPgpMessageException { - StackAlphabet stackItem = automaton.popStack(); - if (stackItem != msg) { - throw new MalformedOpenPgpMessageException(this, input, stackItem); - } - switch (input) { - - case LiteralData: - return LiteralMessage; - - case Signature: - automaton.pushStack(msg); - return OpenPgpMessage; - - case OnePassSignature: - automaton.pushStack(ops); - automaton.pushStack(msg); - return OpenPgpMessage; - - case CompressedData: - return CompressedMessage; - - case EncryptedData: - return EncryptedMessage; - - case EndOfSequence: - default: - throw new MalformedOpenPgpMessageException(this, input, stackItem); - } - } - }, - - LiteralMessage { - @Override - State transition(InputAlphabet input, PDA automaton) throws MalformedOpenPgpMessageException { - StackAlphabet stackItem = automaton.popStack(); - switch (input) { - - case Signature: - if (stackItem == ops) { - return LiteralMessage; - } else { - throw new MalformedOpenPgpMessageException(this, input, stackItem); - } - - case EndOfSequence: - if (stackItem == terminus && automaton.stack.isEmpty()) { - return Valid; - } else { - throw new MalformedOpenPgpMessageException(this, input, stackItem); - } - - case LiteralData: - case OnePassSignature: - case CompressedData: - case EncryptedData: - default: - throw new MalformedOpenPgpMessageException(this, input, stackItem); - } - } - }, - - CompressedMessage { - @Override - State transition(InputAlphabet input, PDA automaton) throws MalformedOpenPgpMessageException { - StackAlphabet stackItem = automaton.popStack(); - switch (input) { - case Signature: - if (stackItem == ops) { - return CompressedMessage; - } else { - throw new MalformedOpenPgpMessageException(this, input, stackItem); - } - - case EndOfSequence: - if (stackItem == terminus && automaton.stack.isEmpty()) { - return Valid; - } else { - throw new MalformedOpenPgpMessageException(this, input, stackItem); - } - - case LiteralData: - case OnePassSignature: - case CompressedData: - case EncryptedData: - default: - throw new MalformedOpenPgpMessageException(this, input, stackItem); - } - } - }, - - EncryptedMessage { - @Override - State transition(InputAlphabet input, PDA automaton) throws MalformedOpenPgpMessageException { - StackAlphabet stackItem = automaton.popStack(); - switch (input) { - case Signature: - if (stackItem == ops) { - return EncryptedMessage; - } else { - throw new MalformedOpenPgpMessageException(this, input, stackItem); - } - - case EndOfSequence: - if (stackItem == terminus && automaton.stack.isEmpty()) { - return Valid; - } else { - throw new MalformedOpenPgpMessageException(this, input, stackItem); - } - - case LiteralData: - case OnePassSignature: - case CompressedData: - case EncryptedData: - default: - throw new MalformedOpenPgpMessageException(this, input, stackItem); - } - } - }, - - Valid { - @Override - State transition(InputAlphabet input, PDA automaton) throws MalformedOpenPgpMessageException { - throw new MalformedOpenPgpMessageException(this, input, null); - } - }, - ; - - /** - * Pop the automatons stack and transition to another state. - * If no valid transition from the current state is available given the popped stack item and input symbol, - * a {@link MalformedOpenPgpMessageException} is thrown. - * Otherwise, the stack is manipulated according to the valid transition and the new state is returned. - * - * @param input input symbol - * @param automaton automaton - * @return new state of the automaton - * @throws MalformedOpenPgpMessageException in case of an illegal input symbol - */ - abstract State transition(InputAlphabet input, PDA automaton) throws MalformedOpenPgpMessageException; - } - private final Stack stack = new Stack<>(); private final List inputs = new ArrayList<>(); // keep track of inputs for debugging / error reporting private State state; + private Syntax syntax = new OpenPgpMessageSyntax(); public PDA() { state = State.OpenPgpMessage; - stack.push(terminus); - stack.push(msg); + pushStack(terminus); + pushStack(msg); } public void next(InputAlphabet input) throws MalformedOpenPgpMessageException { try { - state = state.transition(input, this); + Transition transition = syntax.transition(state, input, popStack()); inputs.add(input); + state = transition.getNewState(); + for (StackAlphabet item : transition.getPushedItems()) { + pushStack(item); + } } catch (MalformedOpenPgpMessageException e) { MalformedOpenPgpMessageException wrapped = new MalformedOpenPgpMessageException("Malformed message: After reading stream " + Arrays.toString(inputs.toArray()) + ", token '" + input + "' is unexpected and illegal.", e); @@ -230,6 +84,9 @@ public class PDA { * @return stack item */ private StackAlphabet popStack() { + if (stack.isEmpty()) { + return null; + } return stack.pop(); } diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/State.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/State.java new file mode 100644 index 00000000..9dee9af1 --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/State.java @@ -0,0 +1,16 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.decryption_verification.syntax_check; + +/** + * Set of states of the automaton. + */ +public enum State { + OpenPgpMessage, + LiteralMessage, + CompressedMessage, + EncryptedMessage, + Valid +} diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/Syntax.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/Syntax.java new file mode 100644 index 00000000..47813a9e --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/Syntax.java @@ -0,0 +1,13 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.decryption_verification.syntax_check; + +import org.pgpainless.exception.MalformedOpenPgpMessageException; + +public interface Syntax { + + Transition transition(State from, InputAlphabet inputAlphabet, StackAlphabet stackItem) + throws MalformedOpenPgpMessageException; +} diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/Transition.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/Transition.java new file mode 100644 index 00000000..bbc28e58 --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/Transition.java @@ -0,0 +1,28 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.decryption_verification.syntax_check; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +public class Transition { + + private final List pushedItems = new ArrayList<>(); + private final State newState; + + public Transition(State newState, StackAlphabet... pushedItems) { + this.newState = newState; + this.pushedItems.addAll(Arrays.asList(pushedItems)); + } + + public State getNewState() { + return newState; + } + + public List getPushedItems() { + return new ArrayList<>(pushedItems); + } +} diff --git a/pgpainless-core/src/main/java/org/pgpainless/exception/MalformedOpenPgpMessageException.java b/pgpainless-core/src/main/java/org/pgpainless/exception/MalformedOpenPgpMessageException.java index aa158746..db8d0df6 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/exception/MalformedOpenPgpMessageException.java +++ b/pgpainless-core/src/main/java/org/pgpainless/exception/MalformedOpenPgpMessageException.java @@ -5,8 +5,8 @@ package org.pgpainless.exception; import org.pgpainless.decryption_verification.syntax_check.InputAlphabet; -import org.pgpainless.decryption_verification.syntax_check.PDA; import org.pgpainless.decryption_verification.syntax_check.StackAlphabet; +import org.pgpainless.decryption_verification.syntax_check.State; /** * Exception that gets thrown if the OpenPGP message is malformed. @@ -20,7 +20,7 @@ public class MalformedOpenPgpMessageException extends RuntimeException { super(message); } - public MalformedOpenPgpMessageException(PDA.State state, InputAlphabet input, StackAlphabet stackItem) { + public MalformedOpenPgpMessageException(State state, InputAlphabet input, StackAlphabet stackItem) { this("There is no legal transition from state '" + state + "' for input '" + input + "' when '" + stackItem + "' is on top of the stack."); } From 3c94f3677f971b5d02b3019bc2b171c1410f6a98 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 27 Oct 2022 12:07:22 +0200 Subject: [PATCH 59/80] Rename *Alphabet to *Symbol and add javadoc --- .../OpenPgpMessageInputStream.java | 18 ++-- .../{InputAlphabet.java => InputSymbol.java} | 2 +- .../syntax_check/OpenPgpMessageSyntax.java | 41 ++++--- .../syntax_check/PDA.java | 18 ++-- .../{StackAlphabet.java => StackSymbol.java} | 2 +- .../syntax_check/Syntax.java | 19 +++- .../syntax_check/Transition.java | 6 +- .../MalformedOpenPgpMessageException.java | 6 +- .../syntax_check/PDATest.java | 102 +++++++++--------- 9 files changed, 121 insertions(+), 93 deletions(-) rename pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/{InputAlphabet.java => InputSymbol.java} (98%) rename pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/{StackAlphabet.java => StackSymbol.java} (93%) 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 75b78294..a0f17210 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 @@ -45,9 +45,9 @@ 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.syntax_check.InputAlphabet; +import org.pgpainless.decryption_verification.syntax_check.InputSymbol; import org.pgpainless.decryption_verification.syntax_check.PDA; -import org.pgpainless.decryption_verification.syntax_check.StackAlphabet; +import org.pgpainless.decryption_verification.syntax_check.StackSymbol; import org.pgpainless.decryption_verification.cleartext_signatures.ClearsignedMessageUtil; import org.pgpainless.decryption_verification.cleartext_signatures.MultiPassStrategy; import org.pgpainless.exception.MalformedOpenPgpMessageException; @@ -334,7 +334,7 @@ public class OpenPgpMessageInputStream extends DecryptionStream { private void processLiteralData() throws IOException { LOGGER.debug("Literal Data Packet at depth " + metadata.depth + " encountered"); - syntaxVerifier.next(InputAlphabet.LiteralData); + syntaxVerifier.next(InputSymbol.LiteralData); PGPLiteralData literalData = packetInputStream.readLiteralData(); this.metadata.setChild(new MessageMetadata.LiteralData( literalData.getFileName(), @@ -344,7 +344,7 @@ public class OpenPgpMessageInputStream extends DecryptionStream { } private void processCompressedData() throws IOException, PGPException { - syntaxVerifier.next(InputAlphabet.CompressedData); + syntaxVerifier.next(InputSymbol.CompressedData); signatures.enterNesting(); PGPCompressedData compressedData = packetInputStream.readCompressedData(); MessageMetadata.CompressedData compressionLayer = new MessageMetadata.CompressedData( @@ -356,7 +356,7 @@ public class OpenPgpMessageInputStream extends DecryptionStream { } private void processOnePassSignature() throws PGPException, IOException { - syntaxVerifier.next(InputAlphabet.OnePassSignature); + syntaxVerifier.next(InputSymbol.OnePassSignature); PGPOnePassSignature onePassSignature = packetInputStream.readOnePassSignature(); LOGGER.debug("One-Pass-Signature Packet by key " + KeyIdUtil.formatKeyId(onePassSignature.getKeyID()) + " at depth " + metadata.depth + " encountered"); @@ -365,8 +365,8 @@ public class OpenPgpMessageInputStream extends DecryptionStream { private void processSignature() throws PGPException, IOException { // true if Signature corresponds to OnePassSignature - boolean isSigForOPS = syntaxVerifier.peekStack() == StackAlphabet.ops; - syntaxVerifier.next(InputAlphabet.Signature); + boolean isSigForOPS = syntaxVerifier.peekStack() == StackSymbol.ops; + syntaxVerifier.next(InputSymbol.Signature); PGPSignature signature; try { signature = packetInputStream.readSignature(); @@ -391,7 +391,7 @@ public class OpenPgpMessageInputStream extends DecryptionStream { private boolean processEncryptedData() throws IOException, PGPException { LOGGER.debug("Symmetrically Encrypted Data Packet at depth " + metadata.depth + " encountered"); - syntaxVerifier.next(InputAlphabet.EncryptedData); + syntaxVerifier.next(InputSymbol.EncryptedData); PGPEncryptedDataList encDataList = packetInputStream.readEncryptedDataList(); // TODO: Replace with !encDataList.isIntegrityProtected() @@ -766,7 +766,7 @@ public class OpenPgpMessageInputStream extends DecryptionStream { } if (packetInputStream != null) { - syntaxVerifier.next(InputAlphabet.EndOfSequence); + syntaxVerifier.next(InputSymbol.EndOfSequence); syntaxVerifier.assertValid(); packetInputStream.close(); } diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/InputAlphabet.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/InputSymbol.java similarity index 98% rename from pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/InputAlphabet.java rename to pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/InputSymbol.java index f73ede34..854c3305 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/InputAlphabet.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/InputSymbol.java @@ -10,7 +10,7 @@ import org.bouncycastle.openpgp.PGPLiteralData; import org.bouncycastle.openpgp.PGPOnePassSignatureList; import org.bouncycastle.openpgp.PGPSignatureList; -public enum InputAlphabet { +public enum InputSymbol { /** * A {@link PGPLiteralData} packet. */ diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/OpenPgpMessageSyntax.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/OpenPgpMessageSyntax.java index ab45a896..4c811e9f 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/OpenPgpMessageSyntax.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/OpenPgpMessageSyntax.java @@ -6,10 +6,20 @@ package org.pgpainless.decryption_verification.syntax_check; import org.pgpainless.exception.MalformedOpenPgpMessageException; +/** + * This class describes the syntax for OpenPGP messages as specified by rfc4880. + * + * @see + * rfc4880 - §11.3. OpenPGP Messages + * @see + * Blog post about theoretic background and translation of grammar to PDA syntax + * @see + * Blog post about practically implementing the PDA for packet syntax validation + */ public class OpenPgpMessageSyntax implements Syntax { @Override - public Transition transition(State from, InputAlphabet input, StackAlphabet stackItem) + public Transition transition(State from, InputSymbol input, StackSymbol stackItem) throws MalformedOpenPgpMessageException { switch (from) { case OpenPgpMessage: @@ -27,9 +37,9 @@ public class OpenPgpMessageSyntax implements Syntax { throw new MalformedOpenPgpMessageException(from, input, stackItem); } - Transition fromOpenPgpMessage(InputAlphabet input, StackAlphabet stackItem) + Transition fromOpenPgpMessage(InputSymbol input, StackSymbol stackItem) throws MalformedOpenPgpMessageException { - if (stackItem != StackAlphabet.msg) { + if (stackItem != StackSymbol.msg) { throw new MalformedOpenPgpMessageException(State.OpenPgpMessage, input, stackItem); } @@ -38,10 +48,10 @@ public class OpenPgpMessageSyntax implements Syntax { return new Transition(State.LiteralMessage); case Signature: - return new Transition(State.OpenPgpMessage, StackAlphabet.msg); + return new Transition(State.OpenPgpMessage, StackSymbol.msg); case OnePassSignature: - return new Transition(State.OpenPgpMessage, StackAlphabet.ops, StackAlphabet.msg); + return new Transition(State.OpenPgpMessage, StackSymbol.ops, StackSymbol.msg); case CompressedData: return new Transition(State.CompressedMessage); @@ -55,17 +65,17 @@ public class OpenPgpMessageSyntax implements Syntax { } } - Transition fromLiteralMessage(InputAlphabet input, StackAlphabet stackItem) + Transition fromLiteralMessage(InputSymbol input, StackSymbol stackItem) throws MalformedOpenPgpMessageException { switch (input) { case Signature: - if (stackItem == StackAlphabet.ops) { + if (stackItem == StackSymbol.ops) { return new Transition(State.LiteralMessage); } break; case EndOfSequence: - if (stackItem == StackAlphabet.terminus) { + if (stackItem == StackSymbol.terminus) { return new Transition(State.Valid); } break; @@ -74,17 +84,17 @@ public class OpenPgpMessageSyntax implements Syntax { throw new MalformedOpenPgpMessageException(State.LiteralMessage, input, stackItem); } - Transition fromCompressedMessage(InputAlphabet input, StackAlphabet stackItem) + Transition fromCompressedMessage(InputSymbol input, StackSymbol stackItem) throws MalformedOpenPgpMessageException { switch (input) { case Signature: - if (stackItem == StackAlphabet.ops) { + if (stackItem == StackSymbol.ops) { return new Transition(State.CompressedMessage); } break; case EndOfSequence: - if (stackItem == StackAlphabet.terminus) { + if (stackItem == StackSymbol.terminus) { return new Transition(State.Valid); } break; @@ -93,17 +103,17 @@ public class OpenPgpMessageSyntax implements Syntax { throw new MalformedOpenPgpMessageException(State.CompressedMessage, input, stackItem); } - Transition fromEncryptedMessage(InputAlphabet input, StackAlphabet stackItem) + Transition fromEncryptedMessage(InputSymbol input, StackSymbol stackItem) throws MalformedOpenPgpMessageException { switch (input) { case Signature: - if (stackItem == StackAlphabet.ops) { + if (stackItem == StackSymbol.ops) { return new Transition(State.EncryptedMessage); } break; case EndOfSequence: - if (stackItem == StackAlphabet.terminus) { + if (stackItem == StackSymbol.terminus) { return new Transition(State.Valid); } break; @@ -112,8 +122,9 @@ public class OpenPgpMessageSyntax implements Syntax { throw new MalformedOpenPgpMessageException(State.EncryptedMessage, input, stackItem); } - Transition fromValid(InputAlphabet input, StackAlphabet stackItem) + Transition fromValid(InputSymbol input, StackSymbol stackItem) throws MalformedOpenPgpMessageException { + // There is no applicable transition rule out of Valid throw new MalformedOpenPgpMessageException(State.Valid, input, stackItem); } } diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/PDA.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/PDA.java index 94c1739e..68a5e2c4 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/PDA.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/PDA.java @@ -13,15 +13,15 @@ import java.util.Arrays; import java.util.List; import java.util.Stack; -import static org.pgpainless.decryption_verification.syntax_check.StackAlphabet.msg; -import static org.pgpainless.decryption_verification.syntax_check.StackAlphabet.terminus; +import static org.pgpainless.decryption_verification.syntax_check.StackSymbol.msg; +import static org.pgpainless.decryption_verification.syntax_check.StackSymbol.terminus; public class PDA { private static final Logger LOGGER = LoggerFactory.getLogger(PDA.class); - private final Stack stack = new Stack<>(); - private final List inputs = new ArrayList<>(); // keep track of inputs for debugging / error reporting + private final Stack stack = new Stack<>(); + private final List inputs = new ArrayList<>(); // keep track of inputs for debugging / error reporting private State state; private Syntax syntax = new OpenPgpMessageSyntax(); @@ -31,12 +31,12 @@ public class PDA { pushStack(msg); } - public void next(InputAlphabet input) throws MalformedOpenPgpMessageException { + public void next(InputSymbol input) throws MalformedOpenPgpMessageException { try { Transition transition = syntax.transition(state, input, popStack()); inputs.add(input); state = transition.getNewState(); - for (StackAlphabet item : transition.getPushedItems()) { + for (StackSymbol item : transition.getPushedItems()) { pushStack(item); } } catch (MalformedOpenPgpMessageException e) { @@ -56,7 +56,7 @@ public class PDA { return state; } - public StackAlphabet peekStack() { + public StackSymbol peekStack() { if (stack.isEmpty()) { return null; } @@ -83,7 +83,7 @@ public class PDA { * * @return stack item */ - private StackAlphabet popStack() { + private StackSymbol popStack() { if (stack.isEmpty()) { return null; } @@ -95,7 +95,7 @@ public class PDA { * * @param item item */ - private void pushStack(StackAlphabet item) { + private void pushStack(StackSymbol item) { stack.push(item); } diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/StackAlphabet.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/StackSymbol.java similarity index 93% rename from pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/StackAlphabet.java rename to pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/StackSymbol.java index a8a2a213..120458e5 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/StackAlphabet.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/StackSymbol.java @@ -4,7 +4,7 @@ package org.pgpainless.decryption_verification.syntax_check; -public enum StackAlphabet { +public enum StackSymbol { /** * OpenPGP Message. */ diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/Syntax.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/Syntax.java index 47813a9e..63d63fed 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/Syntax.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/Syntax.java @@ -6,8 +6,25 @@ package org.pgpainless.decryption_verification.syntax_check; import org.pgpainless.exception.MalformedOpenPgpMessageException; +/** + * This interface can be used to define a custom syntax for the {@link PDA}. + */ public interface Syntax { - Transition transition(State from, InputAlphabet inputAlphabet, StackAlphabet stackItem) + /** + * Describe a transition rule from {@link State}
from
for {@link InputSymbol}
input
+ * with {@link StackSymbol}
stackItem
from the top of the {@link PDA PDAs} stack. + * The resulting {@link Transition} contains the new {@link State}, as well as a list of + * {@link StackSymbol StackSymbols} that get pushed onto the stack by the transition rule. + * If there is no applicable rule, a {@link MalformedOpenPgpMessageException} is thrown, since in this case + * the {@link InputSymbol} must be considered illegal. + * + * @param from current state of the PDA + * @param input input symbol + * @param stackItem item that got popped from the top of the stack + * @return applicable transition rule containing the new state and pushed stack symbols + * @throws MalformedOpenPgpMessageException if there is no applicable transition rule (the input symbol is illegal) + */ + Transition transition(State from, InputSymbol input, StackSymbol stackItem) throws MalformedOpenPgpMessageException; } diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/Transition.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/Transition.java index bbc28e58..a0e58cf0 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/Transition.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/Transition.java @@ -10,10 +10,10 @@ import java.util.List; public class Transition { - private final List pushedItems = new ArrayList<>(); + private final List pushedItems = new ArrayList<>(); private final State newState; - public Transition(State newState, StackAlphabet... pushedItems) { + public Transition(State newState, StackSymbol... pushedItems) { this.newState = newState; this.pushedItems.addAll(Arrays.asList(pushedItems)); } @@ -22,7 +22,7 @@ public class Transition { return newState; } - public List getPushedItems() { + public List getPushedItems() { return new ArrayList<>(pushedItems); } } diff --git a/pgpainless-core/src/main/java/org/pgpainless/exception/MalformedOpenPgpMessageException.java b/pgpainless-core/src/main/java/org/pgpainless/exception/MalformedOpenPgpMessageException.java index db8d0df6..f98a4048 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/exception/MalformedOpenPgpMessageException.java +++ b/pgpainless-core/src/main/java/org/pgpainless/exception/MalformedOpenPgpMessageException.java @@ -4,8 +4,8 @@ package org.pgpainless.exception; -import org.pgpainless.decryption_verification.syntax_check.InputAlphabet; -import org.pgpainless.decryption_verification.syntax_check.StackAlphabet; +import org.pgpainless.decryption_verification.syntax_check.InputSymbol; +import org.pgpainless.decryption_verification.syntax_check.StackSymbol; import org.pgpainless.decryption_verification.syntax_check.State; /** @@ -20,7 +20,7 @@ public class MalformedOpenPgpMessageException extends RuntimeException { super(message); } - public MalformedOpenPgpMessageException(State state, InputAlphabet input, StackAlphabet stackItem) { + public MalformedOpenPgpMessageException(State state, InputSymbol input, StackSymbol stackItem) { this("There is no legal transition from state '" + state + "' for input '" + input + "' when '" + stackItem + "' is on top of the stack."); } diff --git a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/syntax_check/PDATest.java b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/syntax_check/PDATest.java index 9250acfa..2b66eee0 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/syntax_check/PDATest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/syntax_check/PDATest.java @@ -21,8 +21,8 @@ public class PDATest { @Test public void testSimpleLiteralMessageIsValid() throws MalformedOpenPgpMessageException { PDA check = new PDA(); - check.next(InputAlphabet.LiteralData); - check.next(InputAlphabet.EndOfSequence); + check.next(InputSymbol.LiteralData); + check.next(InputSymbol.EndOfSequence); assertTrue(check.isValid()); } @@ -35,10 +35,10 @@ public class PDATest { @Test public void testSimpleOpsSignedMesssageIsValid() throws MalformedOpenPgpMessageException { PDA check = new PDA(); - check.next(InputAlphabet.OnePassSignature); - check.next(InputAlphabet.LiteralData); - check.next(InputAlphabet.Signature); - check.next(InputAlphabet.EndOfSequence); + check.next(InputSymbol.OnePassSignature); + check.next(InputSymbol.LiteralData); + check.next(InputSymbol.Signature); + check.next(InputSymbol.EndOfSequence); assertTrue(check.isValid()); } @@ -52,9 +52,9 @@ public class PDATest { @Test public void testSimplePrependSignedMessageIsValid() throws MalformedOpenPgpMessageException { PDA check = new PDA(); - check.next(InputAlphabet.Signature); - check.next(InputAlphabet.LiteralData); - check.next(InputAlphabet.EndOfSequence); + check.next(InputSymbol.Signature); + check.next(InputSymbol.LiteralData); + check.next(InputSymbol.EndOfSequence); assertTrue(check.isValid()); } @@ -68,11 +68,11 @@ public class PDATest { @Test public void testOPSSignedCompressedMessageIsValid() throws MalformedOpenPgpMessageException { PDA check = new PDA(); - check.next(InputAlphabet.OnePassSignature); - check.next(InputAlphabet.CompressedData); + check.next(InputSymbol.OnePassSignature); + check.next(InputSymbol.CompressedData); // Here would be a nested PDA for the LiteralData packet - check.next(InputAlphabet.Signature); - check.next(InputAlphabet.EndOfSequence); + check.next(InputSymbol.Signature); + check.next(InputSymbol.EndOfSequence); assertTrue(check.isValid()); } @@ -80,105 +80,105 @@ public class PDATest { @Test public void testOPSSignedEncryptedMessageIsValid() { PDA check = new PDA(); - check.next(InputAlphabet.OnePassSignature); - check.next(InputAlphabet.EncryptedData); - check.next(InputAlphabet.Signature); - check.next(InputAlphabet.EndOfSequence); + check.next(InputSymbol.OnePassSignature); + check.next(InputSymbol.EncryptedData); + check.next(InputSymbol.Signature); + check.next(InputSymbol.EndOfSequence); assertTrue(check.isValid()); } @Test public void anyInputAfterEOSIsNotValid() { PDA check = new PDA(); - check.next(InputAlphabet.LiteralData); - check.next(InputAlphabet.EndOfSequence); + check.next(InputSymbol.LiteralData); + check.next(InputSymbol.EndOfSequence); assertThrows(MalformedOpenPgpMessageException.class, - () -> check.next(InputAlphabet.Signature)); + () -> check.next(InputSymbol.Signature)); } @Test public void testEncryptedMessageWithAppendedStandalongSigIsNotValid() { PDA check = new PDA(); - check.next(InputAlphabet.EncryptedData); + check.next(InputSymbol.EncryptedData); assertThrows(MalformedOpenPgpMessageException.class, - () -> check.next(InputAlphabet.Signature)); + () -> check.next(InputSymbol.Signature)); } @Test public void testOPSSignedEncryptedMessageWithMissingSigIsNotValid() { PDA check = new PDA(); - check.next(InputAlphabet.OnePassSignature); - check.next(InputAlphabet.EncryptedData); + check.next(InputSymbol.OnePassSignature); + check.next(InputSymbol.EncryptedData); assertThrows(MalformedOpenPgpMessageException.class, - () -> check.next(InputAlphabet.EndOfSequence)); + () -> check.next(InputSymbol.EndOfSequence)); } @Test public void testTwoLiteralDataIsNotValid() { PDA check = new PDA(); - check.next(InputAlphabet.LiteralData); + check.next(InputSymbol.LiteralData); assertThrows(MalformedOpenPgpMessageException.class, - () -> check.next(InputAlphabet.LiteralData)); + () -> check.next(InputSymbol.LiteralData)); } @Test public void testTrailingSigIsNotValid() { PDA check = new PDA(); - check.next(InputAlphabet.LiteralData); + check.next(InputSymbol.LiteralData); assertThrows(MalformedOpenPgpMessageException.class, - () -> check.next(InputAlphabet.Signature)); + () -> check.next(InputSymbol.Signature)); } @Test public void testOPSAloneIsNotValid() { PDA check = new PDA(); - check.next(InputAlphabet.OnePassSignature); + check.next(InputSymbol.OnePassSignature); assertThrows(MalformedOpenPgpMessageException.class, - () -> check.next(InputAlphabet.EndOfSequence)); + () -> check.next(InputSymbol.EndOfSequence)); } @Test public void testOPSLitWithMissingSigIsNotValid() { PDA check = new PDA(); - check.next(InputAlphabet.OnePassSignature); - check.next(InputAlphabet.LiteralData); + check.next(InputSymbol.OnePassSignature); + check.next(InputSymbol.LiteralData); assertThrows(MalformedOpenPgpMessageException.class, - () -> check.next(InputAlphabet.EndOfSequence)); + () -> check.next(InputSymbol.EndOfSequence)); } @Test public void testCompressedMessageWithStandalongAppendedSigIsNotValid() { PDA check = new PDA(); - check.next(InputAlphabet.CompressedData); + check.next(InputSymbol.CompressedData); assertThrows(MalformedOpenPgpMessageException.class, - () -> check.next(InputAlphabet.Signature)); + () -> check.next(InputSymbol.Signature)); } @Test public void testOPSCompressedDataWithMissingSigIsNotValid() { PDA check = new PDA(); - check.next(InputAlphabet.OnePassSignature); - check.next(InputAlphabet.CompressedData); + check.next(InputSymbol.OnePassSignature); + check.next(InputSymbol.CompressedData); assertThrows(MalformedOpenPgpMessageException.class, - () -> check.next(InputAlphabet.EndOfSequence)); + () -> check.next(InputSymbol.EndOfSequence)); } @Test public void testCompressedMessageFollowedByTrailingLiteralDataIsNotValid() { PDA check = new PDA(); - check.next(InputAlphabet.CompressedData); + check.next(InputSymbol.CompressedData); assertThrows(MalformedOpenPgpMessageException.class, - () -> check.next(InputAlphabet.LiteralData)); + () -> check.next(InputSymbol.LiteralData)); } @Test public void testOPSWithPrependedSigIsValid() { PDA check = new PDA(); - check.next(InputAlphabet.Signature); - check.next(InputAlphabet.OnePassSignature); - check.next(InputAlphabet.LiteralData); - check.next(InputAlphabet.Signature); - check.next(InputAlphabet.EndOfSequence); + check.next(InputSymbol.Signature); + check.next(InputSymbol.OnePassSignature); + check.next(InputSymbol.LiteralData); + check.next(InputSymbol.Signature); + check.next(InputSymbol.EndOfSequence); assertTrue(check.isValid()); } @@ -186,11 +186,11 @@ public class PDATest { @Test public void testPrependedSigInsideOPSSignedMessageIsValid() { PDA check = new PDA(); - check.next(InputAlphabet.OnePassSignature); - check.next(InputAlphabet.Signature); - check.next(InputAlphabet.LiteralData); - check.next(InputAlphabet.Signature); - check.next(InputAlphabet.EndOfSequence); + check.next(InputSymbol.OnePassSignature); + check.next(InputSymbol.Signature); + check.next(InputSymbol.LiteralData); + check.next(InputSymbol.Signature); + check.next(InputSymbol.EndOfSequence); assertTrue(check.isValid()); } From a0c8de57e9b194bed0ea688a6dad65a9106e6ae7 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 27 Oct 2022 12:42:30 +0200 Subject: [PATCH 60/80] More cleanup and better error reporting --- .../syntax_check/OpenPgpMessageSyntax.java | 15 ++++++++----- .../syntax_check/PDA.java | 19 +++++++++++----- .../syntax_check/Syntax.java | 5 ++++- .../syntax_check/Transition.java | 22 ++++++++++++++++++- .../syntax_check/PDATest.java | 2 +- 5 files changed, 48 insertions(+), 15 deletions(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/OpenPgpMessageSyntax.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/OpenPgpMessageSyntax.java index 4c811e9f..c6de8765 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/OpenPgpMessageSyntax.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/OpenPgpMessageSyntax.java @@ -6,6 +6,9 @@ package org.pgpainless.decryption_verification.syntax_check; import org.pgpainless.exception.MalformedOpenPgpMessageException; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + /** * This class describes the syntax for OpenPGP messages as specified by rfc4880. * @@ -19,7 +22,7 @@ import org.pgpainless.exception.MalformedOpenPgpMessageException; public class OpenPgpMessageSyntax implements Syntax { @Override - public Transition transition(State from, InputSymbol input, StackSymbol stackItem) + public @Nonnull Transition transition(@Nonnull State from, @Nonnull InputSymbol input, @Nullable StackSymbol stackItem) throws MalformedOpenPgpMessageException { switch (from) { case OpenPgpMessage: @@ -37,7 +40,7 @@ public class OpenPgpMessageSyntax implements Syntax { throw new MalformedOpenPgpMessageException(from, input, stackItem); } - Transition fromOpenPgpMessage(InputSymbol input, StackSymbol stackItem) + Transition fromOpenPgpMessage(@Nonnull InputSymbol input, @Nullable StackSymbol stackItem) throws MalformedOpenPgpMessageException { if (stackItem != StackSymbol.msg) { throw new MalformedOpenPgpMessageException(State.OpenPgpMessage, input, stackItem); @@ -65,7 +68,7 @@ public class OpenPgpMessageSyntax implements Syntax { } } - Transition fromLiteralMessage(InputSymbol input, StackSymbol stackItem) + Transition fromLiteralMessage(@Nonnull InputSymbol input, @Nullable StackSymbol stackItem) throws MalformedOpenPgpMessageException { switch (input) { case Signature: @@ -84,7 +87,7 @@ public class OpenPgpMessageSyntax implements Syntax { throw new MalformedOpenPgpMessageException(State.LiteralMessage, input, stackItem); } - Transition fromCompressedMessage(InputSymbol input, StackSymbol stackItem) + Transition fromCompressedMessage(@Nonnull InputSymbol input, @Nullable StackSymbol stackItem) throws MalformedOpenPgpMessageException { switch (input) { case Signature: @@ -103,7 +106,7 @@ public class OpenPgpMessageSyntax implements Syntax { throw new MalformedOpenPgpMessageException(State.CompressedMessage, input, stackItem); } - Transition fromEncryptedMessage(InputSymbol input, StackSymbol stackItem) + Transition fromEncryptedMessage(@Nonnull InputSymbol input, @Nullable StackSymbol stackItem) throws MalformedOpenPgpMessageException { switch (input) { case Signature: @@ -122,7 +125,7 @@ public class OpenPgpMessageSyntax implements Syntax { throw new MalformedOpenPgpMessageException(State.EncryptedMessage, input, stackItem); } - Transition fromValid(InputSymbol input, StackSymbol stackItem) + Transition fromValid(@Nonnull InputSymbol input, @Nullable StackSymbol stackItem) throws MalformedOpenPgpMessageException { // There is no applicable transition rule out of Valid throw new MalformedOpenPgpMessageException(State.Valid, input, stackItem); diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/PDA.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/PDA.java index 68a5e2c4..ed9175fd 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/PDA.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/PDA.java @@ -20,10 +20,13 @@ public class PDA { private static final Logger LOGGER = LoggerFactory.getLogger(PDA.class); + // right now we implement what rfc4880 specifies. + // TODO: Consider implementing what we proposed here: + // https://mailarchive.ietf.org/arch/msg/openpgp/uepOF6XpSegMO4c59tt9e5H1i4g/ + private final Syntax syntax = new OpenPgpMessageSyntax(); private final Stack stack = new Stack<>(); - private final List inputs = new ArrayList<>(); // keep track of inputs for debugging / error reporting + private final List inputs = new ArrayList<>(); // Track inputs for debugging / error reporting private State state; - private Syntax syntax = new OpenPgpMessageSyntax(); public PDA() { state = State.OpenPgpMessage; @@ -32,16 +35,20 @@ public class PDA { } public void next(InputSymbol input) throws MalformedOpenPgpMessageException { + StackSymbol stackSymbol = popStack(); try { - Transition transition = syntax.transition(state, input, popStack()); - inputs.add(input); + Transition transition = syntax.transition(state, input, stackSymbol); state = transition.getNewState(); for (StackSymbol item : transition.getPushedItems()) { pushStack(item); } + inputs.add(input); } catch (MalformedOpenPgpMessageException e) { - MalformedOpenPgpMessageException wrapped = new MalformedOpenPgpMessageException("Malformed message: After reading stream " + Arrays.toString(inputs.toArray()) + - ", token '" + input + "' is unexpected and illegal.", e); + MalformedOpenPgpMessageException wrapped = new MalformedOpenPgpMessageException( + "Malformed message: After reading stream " + Arrays.toString(inputs.toArray()) + + ", token '" + input + "' is not allowed." + + "\nNo transition from state '" + state + "' with stack " + Arrays.toString(stack.toArray()) + + (stackSymbol != null ? "||'" + stackSymbol + "'." : "."), e); LOGGER.debug("Invalid input '" + input + "'", wrapped); throw wrapped; } diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/Syntax.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/Syntax.java index 63d63fed..2f3d0a57 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/Syntax.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/Syntax.java @@ -6,6 +6,9 @@ package org.pgpainless.decryption_verification.syntax_check; import org.pgpainless.exception.MalformedOpenPgpMessageException; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + /** * This interface can be used to define a custom syntax for the {@link PDA}. */ @@ -25,6 +28,6 @@ public interface Syntax { * @return applicable transition rule containing the new state and pushed stack symbols * @throws MalformedOpenPgpMessageException if there is no applicable transition rule (the input symbol is illegal) */ - Transition transition(State from, InputSymbol input, StackSymbol stackItem) + @Nonnull Transition transition(@Nonnull State from, @Nonnull InputSymbol input, @Nullable StackSymbol stackItem) throws MalformedOpenPgpMessageException; } diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/Transition.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/Transition.java index a0e58cf0..ab0db5ef 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/Transition.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/Transition.java @@ -4,24 +4,44 @@ package org.pgpainless.decryption_verification.syntax_check; +import javax.annotation.Nonnull; import java.util.ArrayList; import java.util.Arrays; import java.util.List; +/** + * Result of applying a transition rule. + * Transition rules can be described by implementing the {@link Syntax} interface. + */ public class Transition { private final List pushedItems = new ArrayList<>(); private final State newState; - public Transition(State newState, StackSymbol... pushedItems) { + public Transition(@Nonnull State newState, @Nonnull StackSymbol... pushedItems) { this.newState = newState; this.pushedItems.addAll(Arrays.asList(pushedItems)); } + /** + * Return the new {@link State} that is reached by applying the transition. + * + * @return new state + */ + @Nonnull public State getNewState() { return newState; } + /** + * Return a list of {@link StackSymbol StackSymbols} that are pushed onto the stack + * by applying the transition. + * The list contains items in the order in which they are pushed onto the stack. + * The list may be empty. + * + * @return list of items to be pushed onto the stack + */ + @Nonnull public List getPushedItems() { return new ArrayList<>(pushedItems); } diff --git a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/syntax_check/PDATest.java b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/syntax_check/PDATest.java index 2b66eee0..d0486a3e 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/syntax_check/PDATest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/syntax_check/PDATest.java @@ -97,7 +97,7 @@ public class PDATest { } @Test - public void testEncryptedMessageWithAppendedStandalongSigIsNotValid() { + public void testEncryptedMessageWithAppendedStandaloneSigIsNotValid() { PDA check = new PDA(); check.next(InputSymbol.EncryptedData); assertThrows(MalformedOpenPgpMessageException.class, From 291f59b9e4e990fbeb7f8078c806e9dfafdcda05 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 27 Oct 2022 12:47:42 +0200 Subject: [PATCH 61/80] Clean up old unused code --- .../DecryptionBuilderInterface.java | 2 +- .../DecryptionStreamImpl.java | 65 ------ .../SignatureInputStream.java | 215 ------------------ .../exception/FinalIOException.java | 17 -- .../MissingLiteralDataException.java | 17 -- 5 files changed, 1 insertion(+), 315 deletions(-) delete mode 100644 pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionStreamImpl.java delete mode 100644 pgpainless-core/src/main/java/org/pgpainless/decryption_verification/SignatureInputStream.java delete mode 100644 pgpainless-core/src/main/java/org/pgpainless/exception/FinalIOException.java delete mode 100644 pgpainless-core/src/main/java/org/pgpainless/exception/MissingLiteralDataException.java diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionBuilderInterface.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionBuilderInterface.java index b35911de..07db42f0 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionBuilderInterface.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionBuilderInterface.java @@ -13,7 +13,7 @@ import org.bouncycastle.openpgp.PGPException; public interface DecryptionBuilderInterface { /** - * Create a {@link DecryptionStreamImpl} on an {@link InputStream} which contains the encrypted and/or signed data. + * Create a {@link DecryptionStream} on an {@link InputStream} which contains the encrypted and/or signed data. * * @param inputStream encrypted and/or signed data. * @return api handle diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionStreamImpl.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionStreamImpl.java deleted file mode 100644 index 27ace697..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionStreamImpl.java +++ /dev/null @@ -1,65 +0,0 @@ -// SPDX-FileCopyrightText: 2018 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.decryption_verification; - -import java.io.IOException; -import java.io.InputStream; -import javax.annotation.Nonnull; - -import org.bouncycastle.util.io.Streams; - -/** - * Decryption Stream that handles updating and verification of detached signatures, - * as well as verification of integrity-protected input streams once the stream gets closed. - */ -public class DecryptionStreamImpl extends DecryptionStream { - - private final InputStream inputStream; - private final IntegrityProtectedInputStream integrityProtectedInputStream; - private final InputStream armorStream; - - /** - * Create an input stream that handles decryption and - if necessary - integrity protection verification. - * - * @param wrapped underlying input stream - * @param resultBuilder builder for decryption metadata like algorithms, recipients etc. - * @param integrityProtectedInputStream in case of data encrypted using SEIP packet close this stream to check integrity - * @param armorStream armor stream to verify CRC checksums - */ - DecryptionStreamImpl(@Nonnull InputStream wrapped, - @Nonnull OpenPgpMetadata.Builder resultBuilder, - IntegrityProtectedInputStream integrityProtectedInputStream, - InputStream armorStream) { - super(resultBuilder); - this.inputStream = wrapped; - this.integrityProtectedInputStream = integrityProtectedInputStream; - this.armorStream = armorStream; - } - - @Override - public void close() throws IOException { - if (armorStream != null) { - Streams.drain(armorStream); - } - inputStream.close(); - if (integrityProtectedInputStream != null) { - integrityProtectedInputStream.close(); - } - super.close(); - } - - @Override - public int read() throws IOException { - int r = inputStream.read(); - return r; - } - - @Override - public int read(@Nonnull byte[] bytes, int offset, int length) throws IOException { - int read = inputStream.read(bytes, offset, length); - return read; - } - -} 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 deleted file mode 100644 index 70a2f4ef..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/SignatureInputStream.java +++ /dev/null @@ -1,215 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.decryption_verification; - -import static org.pgpainless.signature.consumer.SignatureValidator.signatureWasCreatedInBounds; - -import java.io.FilterInputStream; -import java.io.IOException; -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; -import org.bouncycastle.openpgp.PGPSignature; -import org.bouncycastle.openpgp.PGPSignatureList; -import org.pgpainless.PGPainless; -import org.pgpainless.exception.SignatureValidationException; -import org.pgpainless.policy.Policy; -import org.pgpainless.signature.consumer.CertificateValidator; -import org.pgpainless.signature.consumer.SignatureCheck; -import org.pgpainless.signature.consumer.OnePassSignatureCheck; -import org.pgpainless.signature.SignatureUtils; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -public abstract class SignatureInputStream extends FilterInputStream { - - protected SignatureInputStream(InputStream inputStream) { - super(inputStream); - } - - public static class VerifySignatures extends SignatureInputStream { - - private static final Logger LOGGER = LoggerFactory.getLogger(VerifySignatures.class); - - private final PGPObjectFactory objectFactory; - private final List opSignatures; - private final Map opSignaturesWithMissingCert; - private final List detachedSignatures; - private final ConsumerOptions options; - private final OpenPgpMetadata.Builder resultBuilder; - - public VerifySignatures( - InputStream literalDataStream, - @Nullable PGPObjectFactory objectFactory, - List opSignatures, - Map onePassSignaturesWithMissingCert, - List detachedSignatures, - ConsumerOptions options, - OpenPgpMetadata.Builder resultBuilder) { - super(literalDataStream); - this.objectFactory = objectFactory; - this.opSignatures = opSignatures; - this.opSignaturesWithMissingCert = onePassSignaturesWithMissingCert; - this.detachedSignatures = detachedSignatures; - this.options = options; - this.resultBuilder = resultBuilder; - } - - @Override - public int read() throws IOException { - final int data = super.read(); - final boolean endOfStream = data == -1; - if (endOfStream) { - finalizeSignatures(); - } else { - byte b = (byte) data; - updateOnePassSignatures(b); - updateDetachedSignatures(b); - } - return data; - } - - @Override - public int read(@Nonnull byte[] b, int off, int len) throws IOException { - int read = super.read(b, off, len); - - final boolean endOfStream = read == -1; - if (endOfStream) { - finalizeSignatures(); - } else { - updateOnePassSignatures(b, off, read); - updateDetachedSignatures(b, off, read); - } - return read; - } - - private void finalizeSignatures() { - parseAndCombineSignatures(); - verifyOnePassSignatures(); - verifyDetachedSignatures(); - } - - public void parseAndCombineSignatures() { - if (objectFactory == null) { - return; - } - // Parse signatures from message - PGPSignatureList signatures; - try { - signatures = parseSignatures(objectFactory); - } catch (IOException e) { - return; - } - List signatureList = SignatureUtils.toList(signatures); - // Set signatures as comparison sigs in OPS checks - for (int i = 0; i < opSignatures.size(); i++) { - int reversedIndex = opSignatures.size() - i - 1; - opSignatures.get(i).setSignature(signatureList.get(reversedIndex)); - } - - for (PGPSignature signature : signatureList) { - if (opSignaturesWithMissingCert.containsKey(signature.getKeyID())) { - OnePassSignatureCheck check = opSignaturesWithMissingCert.remove(signature.getKeyID()); - check.setSignature(signature); - - resultBuilder.addInvalidInbandSignature(new SignatureVerification(signature, null), - new SignatureValidationException( - "Missing verification certificate " + Long.toHexString(signature.getKeyID()))); - } - } - } - - private PGPSignatureList parseSignatures(PGPObjectFactory objectFactory) throws IOException { - PGPSignatureList signatureList = null; - Object pgpObject = objectFactory.nextObject(); - while (pgpObject != null && signatureList == null) { - if (pgpObject instanceof PGPSignatureList) { - signatureList = (PGPSignatureList) pgpObject; - } else { - pgpObject = objectFactory.nextObject(); - } - } - - if (signatureList == null || signatureList.isEmpty()) { - throw new IOException("Verification failed - No Signatures found"); - } - - return signatureList; - } - - - private synchronized void verifyOnePassSignatures() { - Policy policy = PGPainless.getPolicy(); - for (OnePassSignatureCheck opSignature : opSignatures) { - if (opSignature.getSignature() == null) { - LOGGER.warn("Found OnePassSignature without respective signature packet -> skip"); - continue; - } - - try { - signatureWasCreatedInBounds(options.getVerifyNotBefore(), - options.getVerifyNotAfter()).verify(opSignature.getSignature()); - CertificateValidator.validateCertificateAndVerifyOnePassSignature(opSignature, policy); - resultBuilder.addVerifiedInbandSignature( - new SignatureVerification(opSignature.getSignature(), opSignature.getSigningKey())); - } catch (SignatureValidationException e) { - LOGGER.warn("One-pass-signature verification failed for signature made by key {}: {}", - opSignature.getSigningKey(), e.getMessage(), e); - resultBuilder.addInvalidInbandSignature( - new SignatureVerification(opSignature.getSignature(), opSignature.getSigningKey()), e); - } - } - } - - private void verifyDetachedSignatures() { - Policy policy = PGPainless.getPolicy(); - for (SignatureCheck s : detachedSignatures) { - try { - signatureWasCreatedInBounds(options.getVerifyNotBefore(), - options.getVerifyNotAfter()).verify(s.getSignature()); - CertificateValidator.validateCertificateAndVerifyInitializedSignature(s.getSignature(), - (PGPPublicKeyRing) s.getSigningKeyRing(), policy); - resultBuilder.addVerifiedDetachedSignature(new SignatureVerification(s.getSignature(), - s.getSigningKeyIdentifier())); - } catch (SignatureValidationException e) { - LOGGER.warn("One-pass-signature verification failed for signature made by key {}: {}", - s.getSigningKeyIdentifier(), e.getMessage(), e); - resultBuilder.addInvalidDetachedSignature(new SignatureVerification(s.getSignature(), - s.getSigningKeyIdentifier()), e); - } - } - } - - private void updateOnePassSignatures(byte data) { - for (OnePassSignatureCheck opSignature : opSignatures) { - opSignature.getOnePassSignature().update(data); - } - } - - private void updateOnePassSignatures(byte[] bytes, int offset, int length) { - for (OnePassSignatureCheck opSignature : opSignatures) { - opSignature.getOnePassSignature().update(bytes, offset, length); - } - } - - private void updateDetachedSignatures(byte b) { - for (SignatureCheck detachedSignature : detachedSignatures) { - detachedSignature.getSignature().update(b); - } - } - - private void updateDetachedSignatures(byte[] b, int off, int read) { - for (SignatureCheck detachedSignature : detachedSignatures) { - detachedSignature.getSignature().update(b, off, read); - } - } - - } -} diff --git a/pgpainless-core/src/main/java/org/pgpainless/exception/FinalIOException.java b/pgpainless-core/src/main/java/org/pgpainless/exception/FinalIOException.java deleted file mode 100644 index 6b6f86de..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/exception/FinalIOException.java +++ /dev/null @@ -1,17 +0,0 @@ -// 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/main/java/org/pgpainless/exception/MissingLiteralDataException.java b/pgpainless-core/src/main/java/org/pgpainless/exception/MissingLiteralDataException.java deleted file mode 100644 index e396b1df..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/exception/MissingLiteralDataException.java +++ /dev/null @@ -1,17 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.exception; - -import org.bouncycastle.openpgp.PGPException; - -/** - * Exception that gets thrown if a {@link org.bouncycastle.bcpg.LiteralDataPacket} is expected, but not found. - */ -public class MissingLiteralDataException extends PGPException { - - public MissingLiteralDataException(String message) { - super(message); - } -} From 6716e363c76f84d63673bae68bec06ffe4e8ea1d Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 27 Oct 2022 13:13:27 +0200 Subject: [PATCH 62/80] Allow injection of different syntax into PDA --- .../syntax_check/PDA.java | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/PDA.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/PDA.java index ed9175fd..65210ce0 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/PDA.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/PDA.java @@ -8,6 +8,7 @@ import org.pgpainless.exception.MalformedOpenPgpMessageException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import javax.annotation.Nonnull; import java.util.ArrayList; import java.util.Arrays; import java.util.List; @@ -23,15 +24,24 @@ public class PDA { // right now we implement what rfc4880 specifies. // TODO: Consider implementing what we proposed here: // https://mailarchive.ietf.org/arch/msg/openpgp/uepOF6XpSegMO4c59tt9e5H1i4g/ - private final Syntax syntax = new OpenPgpMessageSyntax(); + private final Syntax syntax; private final Stack stack = new Stack<>(); private final List inputs = new ArrayList<>(); // Track inputs for debugging / error reporting private State state; + /** + * + */ public PDA() { - state = State.OpenPgpMessage; - pushStack(terminus); - pushStack(msg); + this(new OpenPgpMessageSyntax(), State.OpenPgpMessage, terminus, msg); + } + + public PDA(@Nonnull Syntax syntax, @Nonnull State initialState, @Nonnull StackSymbol... initialStack) { + this.syntax = syntax; + this.state = initialState; + for (StackSymbol symbol : initialStack) { + pushStack(symbol); + } } public void next(InputSymbol input) throws MalformedOpenPgpMessageException { From d5fcb3bc29137cce81d09f5f3024dea90ac2efcf Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 27 Oct 2022 13:53:54 +0200 Subject: [PATCH 63/80] Add (commented-out) read(buf, off, len) implementation for DelayedTeeInputStream --- .../TeeBCPGInputStream.java | 32 +++++++++++++++---- 1 file changed, 26 insertions(+), 6 deletions(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/TeeBCPGInputStream.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/TeeBCPGInputStream.java index bbcf593e..52ec9001 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/TeeBCPGInputStream.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/TeeBCPGInputStream.java @@ -26,20 +26,20 @@ import org.pgpainless.algorithm.OpenPgpPacket; * {@link BCPGInputStream#readPacket()} inconsistently calls a mix of {@link BCPGInputStream#read()} and * {@link InputStream#read()} of the underlying stream. This would cause the second length byte to get swallowed up. * - * Therefore, this class delegates the teeing to an {@link DelayedTeeInputStreamInputStream} which wraps the underlying + * Therefore, this class delegates the teeing to an {@link DelayedTeeInputStream} which wraps the underlying * stream. Since calling {@link BCPGInputStream#nextPacketTag()} reads up to and including the next packets tag, * we need to delay teeing out that byte to signature verifiers. * Hence, the reading methods of the {@link TeeBCPGInputStream} handle pushing this byte to the output stream using - * {@link DelayedTeeInputStreamInputStream#squeeze()}. + * {@link DelayedTeeInputStream#squeeze()}. */ public class TeeBCPGInputStream { - protected final DelayedTeeInputStreamInputStream delayedTee; + protected final DelayedTeeInputStream delayedTee; // InputStream of OpenPGP packets of the current layer protected final BCPGInputStream packetInputStream; public TeeBCPGInputStream(BCPGInputStream inputStream, OutputStream outputStream) { - this.delayedTee = new DelayedTeeInputStreamInputStream(inputStream, outputStream); + this.delayedTee = new DelayedTeeInputStream(inputStream, outputStream); this.packetInputStream = BCPGInputStream.wrap(delayedTee); } @@ -100,13 +100,13 @@ public class TeeBCPGInputStream { this.packetInputStream.close(); } - public static class DelayedTeeInputStreamInputStream extends InputStream { + public static class DelayedTeeInputStream extends InputStream { private int last = -1; private final InputStream inputStream; private final OutputStream outputStream; - public DelayedTeeInputStreamInputStream(InputStream inputStream, OutputStream outputStream) { + public DelayedTeeInputStream(InputStream inputStream, OutputStream outputStream) { this.inputStream = inputStream; this.outputStream = outputStream; } @@ -127,6 +127,26 @@ public class TeeBCPGInputStream { } } + // TODO: Uncomment, once BC-172.1 is available + // see https://github.com/bcgit/bc-java/issues/1257 + /* + @Override + public int read(byte[] b, int off, int len) throws IOException { + if (last != -1) { + outputStream.write(last); + } + + int r = inputStream.read(b, off, len); + if (r > 0) { + outputStream.write(b, off, r - 1); + last = b[off + r - 1]; + } else { + last = -1; + } + return r; + } + */ + /** * Squeeze the last byte out and update the output stream. * From d94fa6b74b9f6fa86bbed01f01b621b2dd6b3361 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 27 Oct 2022 13:55:58 +0200 Subject: [PATCH 64/80] Add comments --- .../OpenPgpMessageInputStream.java | 14 ++++++++++++-- .../decryption_verification/syntax_check/PDA.java | 2 +- 2 files changed, 13 insertions(+), 3 deletions(-) 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 a0f17210..1b11744c 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 @@ -116,6 +116,7 @@ public class OpenPgpMessageInputStream extends DecryptionStream { /** * Create an {@link OpenPgpMessageInputStream} suitable for decryption and verification of * OpenPGP messages and signatures. + * This factory method takes a custom {@link Policy} instead of using the global policy object. * * @param inputStream underlying input stream containing the OpenPGP message * @param options options for consuming the message @@ -161,7 +162,7 @@ public class OpenPgpMessageInputStream extends DecryptionStream { armorIn, options, metadata, policy); } } else { - throw new AssertionError("Huh?"); + throw new AssertionError("Cannot deduce type of data."); } } @@ -212,6 +213,7 @@ public class OpenPgpMessageInputStream extends DecryptionStream { switch (type) { + // Binary OpenPGP Message case standard: // tee out packet bytes for signature verification packetInputStream = new TeeBCPGInputStream(BCPGInputStream.wrap(inputStream), this.signatures); @@ -220,6 +222,7 @@ public class OpenPgpMessageInputStream extends DecryptionStream { consumePackets(); break; + // Cleartext Signature Framework (probably signed message) case cleartext_signed: resultBuilder.setCleartextSigned(); MultiPassStrategy multiPassStrategy = options.getMultiPassStrategy(); @@ -235,6 +238,7 @@ public class OpenPgpMessageInputStream extends DecryptionStream { nestedInputStream = new TeeInputStream(multiPassStrategy.getMessageInputStream(), this.signatures); break; + // Non-OpenPGP Data (e.g. detached signature verification) case non_openpgp: packetInputStream = null; nestedInputStream = new TeeInputStream(inputStream, this.signatures); @@ -265,7 +269,7 @@ public class OpenPgpMessageInputStream extends DecryptionStream { return; } - loop: // we break this when we go deeper. + loop: // we break this when we enter nested packets and later resume while ((nextPacket = packetInputStream.nextPacketTag()) != null) { signatures.nextPacket(nextPacket); switch (nextPacket) { @@ -296,6 +300,7 @@ public class OpenPgpMessageInputStream extends DecryptionStream { case SED: case SEIPD: if (processEncryptedData()) { + // Successfully decrypted, enter nested content break loop; } @@ -336,10 +341,12 @@ public class OpenPgpMessageInputStream extends DecryptionStream { LOGGER.debug("Literal Data Packet at depth " + metadata.depth + " encountered"); syntaxVerifier.next(InputSymbol.LiteralData); PGPLiteralData literalData = packetInputStream.readLiteralData(); + // Extract Metadata this.metadata.setChild(new MessageMetadata.LiteralData( literalData.getFileName(), literalData.getModificationTime(), StreamEncoding.requireFromCode(literalData.getFormat()))); + nestedInputStream = literalData.getDataStream(); } @@ -347,9 +354,11 @@ public class OpenPgpMessageInputStream extends DecryptionStream { syntaxVerifier.next(InputSymbol.CompressedData); signatures.enterNesting(); PGPCompressedData compressedData = packetInputStream.readCompressedData(); + // Extract Metadata MessageMetadata.CompressedData compressionLayer = new MessageMetadata.CompressedData( CompressionAlgorithm.fromId(compressedData.getAlgorithm()), metadata.depth + 1); + LOGGER.debug("Compressed Data Packet (" + compressionLayer.algorithm + ") at depth " + metadata.depth + " encountered"); InputStream decompressed = compressedData.getDataStream(); nestedInputStream = new OpenPgpMessageInputStream(decompressed, options, compressionLayer, policy); @@ -374,6 +383,7 @@ public class OpenPgpMessageInputStream extends DecryptionStream { LOGGER.debug("Unsupported Signature at depth " + metadata.depth + " encountered.", e); return; } + long keyId = SignatureUtils.determineIssuerKeyId(signature); if (isSigForOPS) { LOGGER.debug("Signature Packet corresponding to One-Pass-Signature by key " + diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/PDA.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/PDA.java index 65210ce0..07a5fdcb 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/PDA.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/PDA.java @@ -30,7 +30,7 @@ public class PDA { private State state; /** - * + * Default constructor which initializes the PDA to work with the {@link OpenPgpMessageSyntax}. */ public PDA() { this(new OpenPgpMessageSyntax(), State.OpenPgpMessage, terminus, msg); From 1be4d8e5c427cfc247e5a2b5eb1f2c70927cf3ce Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Fri, 23 Sep 2022 14:51:06 +0200 Subject: [PATCH 65/80] Implement caching PublicKeyDataDecryptorFactory --- .../CachingPublicKeyDataDecryptorFactory.java | 75 +++++++++++++++++++ .../java/org/bouncycastle/package-info.java | 8 ++ 2 files changed, 83 insertions(+) create mode 100644 pgpainless-core/src/main/java/org/bouncycastle/CachingPublicKeyDataDecryptorFactory.java create mode 100644 pgpainless-core/src/main/java/org/bouncycastle/package-info.java diff --git a/pgpainless-core/src/main/java/org/bouncycastle/CachingPublicKeyDataDecryptorFactory.java b/pgpainless-core/src/main/java/org/bouncycastle/CachingPublicKeyDataDecryptorFactory.java new file mode 100644 index 00000000..1498b6f2 --- /dev/null +++ b/pgpainless-core/src/main/java/org/bouncycastle/CachingPublicKeyDataDecryptorFactory.java @@ -0,0 +1,75 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.bouncycastle; + +import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.operator.PGPDataDecryptor; +import org.bouncycastle.openpgp.operator.PublicKeyDataDecryptorFactory; +import org.bouncycastle.util.encoders.Base64; + +import java.util.HashMap; +import java.util.Map; + +/** + * Implementation of the {@link PublicKeyDataDecryptorFactory} which caches decrypted session keys. + * That way, if a message needs to be decrypted multiple times, expensive private key operations can be omitted. + * + * This implementation changes the behavior or {@link #recoverSessionData(int, byte[][])} to first return any + * cache hits. + * If no hit is found, the method call is delegated to the underlying {@link PublicKeyDataDecryptorFactory}. + * The result of that is then placed in the cache and returned. + * + * TODO: Do we also cache invalid session keys? + */ +public class CachingPublicKeyDataDecryptorFactory implements PublicKeyDataDecryptorFactory { + + private final Map cachedSessionKeys = new HashMap<>(); + private final PublicKeyDataDecryptorFactory factory; + + public CachingPublicKeyDataDecryptorFactory(PublicKeyDataDecryptorFactory factory) { + this.factory = factory; + } + + @Override + public byte[] recoverSessionData(int keyAlgorithm, byte[][] secKeyData) throws PGPException { + byte[] sessionKey = lookup(secKeyData); + if (sessionKey == null) { + sessionKey = factory.recoverSessionData(keyAlgorithm, secKeyData); + cache(secKeyData, sessionKey); + } + return sessionKey; + } + + private byte[] lookup(byte[][] secKeyData) { + byte[] sk = secKeyData[0]; + String key = Base64.toBase64String(sk); + byte[] sessionKey = cachedSessionKeys.get(key); + return copy(sessionKey); + } + + private void cache(byte[][] secKeyData, byte[] sessionKey) { + byte[] sk = secKeyData[0]; + String key = Base64.toBase64String(sk); + cachedSessionKeys.put(key, copy(sessionKey)); + } + + private static byte[] copy(byte[] bytes) { + if (bytes == null) { + return null; + } + byte[] copy = new byte[bytes.length]; + System.arraycopy(bytes, 0, copy, 0, copy.length); + return copy; + } + + public void clear() { + cachedSessionKeys.clear(); + } + + @Override + public PGPDataDecryptor createDataDecryptor(boolean withIntegrityPacket, int encAlgorithm, byte[] key) throws PGPException { + return null; + } +} diff --git a/pgpainless-core/src/main/java/org/bouncycastle/package-info.java b/pgpainless-core/src/main/java/org/bouncycastle/package-info.java new file mode 100644 index 00000000..565bb5f4 --- /dev/null +++ b/pgpainless-core/src/main/java/org/bouncycastle/package-info.java @@ -0,0 +1,8 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +/** + * Classes which could be upstreamed to BC at some point. + */ +package org.bouncycastle; From 09aaa9fa4ed76db1400c413c3fcf51a59c10b711 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Fri, 28 Oct 2022 14:56:06 +0200 Subject: [PATCH 66/80] Fix CachingBcPublicKeyDataDecryptorFactory --- ...chingBcPublicKeyDataDecryptorFactory.java} | 44 ++++++--- ...ngBcPublicKeyDataDecryptorFactoryTest.java | 96 +++++++++++++++++++ 2 files changed, 126 insertions(+), 14 deletions(-) rename pgpainless-core/src/main/java/org/bouncycastle/{CachingPublicKeyDataDecryptorFactory.java => CachingBcPublicKeyDataDecryptorFactory.java} (52%) create mode 100644 pgpainless-core/src/test/java/bouncycastle/CachingBcPublicKeyDataDecryptorFactoryTest.java diff --git a/pgpainless-core/src/main/java/org/bouncycastle/CachingPublicKeyDataDecryptorFactory.java b/pgpainless-core/src/main/java/org/bouncycastle/CachingBcPublicKeyDataDecryptorFactory.java similarity index 52% rename from pgpainless-core/src/main/java/org/bouncycastle/CachingPublicKeyDataDecryptorFactory.java rename to pgpainless-core/src/main/java/org/bouncycastle/CachingBcPublicKeyDataDecryptorFactory.java index 1498b6f2..3c967224 100644 --- a/pgpainless-core/src/main/java/org/bouncycastle/CachingPublicKeyDataDecryptorFactory.java +++ b/pgpainless-core/src/main/java/org/bouncycastle/CachingBcPublicKeyDataDecryptorFactory.java @@ -5,9 +5,15 @@ package org.bouncycastle; import org.bouncycastle.openpgp.PGPException; -import org.bouncycastle.openpgp.operator.PGPDataDecryptor; +import org.bouncycastle.openpgp.PGPPrivateKey; import org.bouncycastle.openpgp.operator.PublicKeyDataDecryptorFactory; +import org.bouncycastle.openpgp.operator.bc.BcPublicKeyDataDecryptorFactory; import org.bouncycastle.util.encoders.Base64; +import org.bouncycastle.util.encoders.Hex; +import org.pgpainless.decryption_verification.CustomPublicKeyDataDecryptorFactory; +import org.pgpainless.key.SubkeyIdentifier; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import java.util.HashMap; import java.util.Map; @@ -20,36 +26,46 @@ import java.util.Map; * cache hits. * If no hit is found, the method call is delegated to the underlying {@link PublicKeyDataDecryptorFactory}. * The result of that is then placed in the cache and returned. - * - * TODO: Do we also cache invalid session keys? */ -public class CachingPublicKeyDataDecryptorFactory implements PublicKeyDataDecryptorFactory { +public class CachingBcPublicKeyDataDecryptorFactory + extends BcPublicKeyDataDecryptorFactory + implements CustomPublicKeyDataDecryptorFactory { + + private static final Logger LOGGER = LoggerFactory.getLogger(CachingBcPublicKeyDataDecryptorFactory.class); private final Map cachedSessionKeys = new HashMap<>(); - private final PublicKeyDataDecryptorFactory factory; + private final SubkeyIdentifier decryptionKey; - public CachingPublicKeyDataDecryptorFactory(PublicKeyDataDecryptorFactory factory) { - this.factory = factory; + public CachingBcPublicKeyDataDecryptorFactory(PGPPrivateKey privateKey, SubkeyIdentifier decryptionKey) { + super(privateKey); + this.decryptionKey = decryptionKey; } @Override public byte[] recoverSessionData(int keyAlgorithm, byte[][] secKeyData) throws PGPException { - byte[] sessionKey = lookup(secKeyData); + byte[] sessionKey = lookupSessionKeyData(secKeyData); if (sessionKey == null) { - sessionKey = factory.recoverSessionData(keyAlgorithm, secKeyData); - cache(secKeyData, sessionKey); + LOGGER.debug("Cache miss for encrypted session key " + Hex.toHexString(secKeyData[0])); + sessionKey = costlyRecoverSessionData(keyAlgorithm, secKeyData); + cacheSessionKeyData(secKeyData, sessionKey); + } else { + LOGGER.debug("Cache hit for encrypted session key " + Hex.toHexString(secKeyData[0])); } return sessionKey; } - private byte[] lookup(byte[][] secKeyData) { + public byte[] costlyRecoverSessionData(int keyAlgorithm, byte[][] secKeyData) throws PGPException { + return super.recoverSessionData(keyAlgorithm, secKeyData); + } + + private byte[] lookupSessionKeyData(byte[][] secKeyData) { byte[] sk = secKeyData[0]; String key = Base64.toBase64String(sk); byte[] sessionKey = cachedSessionKeys.get(key); return copy(sessionKey); } - private void cache(byte[][] secKeyData, byte[] sessionKey) { + private void cacheSessionKeyData(byte[][] secKeyData, byte[] sessionKey) { byte[] sk = secKeyData[0]; String key = Base64.toBase64String(sk); cachedSessionKeys.put(key, copy(sessionKey)); @@ -69,7 +85,7 @@ public class CachingPublicKeyDataDecryptorFactory implements PublicKeyDataDecryp } @Override - public PGPDataDecryptor createDataDecryptor(boolean withIntegrityPacket, int encAlgorithm, byte[] key) throws PGPException { - return null; + public SubkeyIdentifier getSubkeyIdentifier() { + return decryptionKey; } } diff --git a/pgpainless-core/src/test/java/bouncycastle/CachingBcPublicKeyDataDecryptorFactoryTest.java b/pgpainless-core/src/test/java/bouncycastle/CachingBcPublicKeyDataDecryptorFactoryTest.java new file mode 100644 index 00000000..186bcbc2 --- /dev/null +++ b/pgpainless-core/src/test/java/bouncycastle/CachingBcPublicKeyDataDecryptorFactoryTest.java @@ -0,0 +1,96 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package bouncycastle; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.security.InvalidAlgorithmParameterException; +import java.security.NoSuchAlgorithmException; + +import org.bouncycastle.CachingBcPublicKeyDataDecryptorFactory; +import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.PGPPrivateKey; +import org.bouncycastle.openpgp.PGPSecretKey; +import org.bouncycastle.openpgp.PGPSecretKeyRing; +import org.bouncycastle.util.io.Streams; +import org.junit.jupiter.api.Test; +import org.pgpainless.PGPainless; +import org.pgpainless.algorithm.EncryptionPurpose; +import org.pgpainless.decryption_verification.ConsumerOptions; +import org.pgpainless.decryption_verification.DecryptionStream; +import org.pgpainless.key.SubkeyIdentifier; +import org.pgpainless.key.info.KeyRingInfo; +import org.pgpainless.key.protection.SecretKeyRingProtector; +import org.pgpainless.key.protection.UnlockSecretKey; + +public class CachingBcPublicKeyDataDecryptorFactoryTest { + + private static final String KEY = "-----BEGIN PGP PRIVATE KEY BLOCK-----\n" + + "Version: PGPainless\n" + + "Comment: C8AE 4279 5958 5F46 86A9 8B5F EC69 7C29 2BE4 44E0\n" + + "Comment: Alice\n" + + "\n" + + "lFgEY1vEcxYJKwYBBAHaRw8BAQdAXOUK1uc1iBeM+mMt2nLCukXWoJd/SodrtN9S\n" + + "U/zzwu0AAP9eePPw91KLuq6PF9jQoTRz/cW4CyiALNJpsOJIZ1rp3xOBtAVBbGlj\n" + + "ZYiPBBMWCgBBBQJjW8RzCRDsaXwpK+RE4BYhBMiuQnlZWF9GhqmLX+xpfCkr5ETg\n" + + "Ap4BApsBBRYCAwEABAsJCAcFFQoJCAsCmQEAAGqWAQC8oz7l8izjUis5ji+sgI+q\n" + + "gML22VNybqmLBpzZwnNU5wEApe9fNTRbK5yAITGBscxH7o74Qe+CLI6Ni5MwzKxr\n" + + "5AucXQRjW8RzEgorBgEEAZdVAQUBAQdAm8xk0QSvpp2ZU1KQ31E7eEZYLKpbW4JE\n" + + "opmtMQx6AlIDAQgHAAD/XTb/qSosfkNvli3BQiUzVRAqKaU4PKAq7at6afxoYSgN\n" + + "4Yh1BBgWCgAdBQJjW8RzAp4BApsMBRYCAwEABAsJCAcFFQoJCAsACgkQ7Gl8KSvk\n" + + "ROB38QEA0MvDt0bjEXwFoM0E34z0MtPcG3VBYcQ+iFRIqFfEl5UA/2yZxFjoZqrs\n" + + "AQE8TaVpXYfbc2p/GEKA9LGd9l/g0QQLnFgEY1vEcxYJKwYBBAHaRw8BAQdAyCOv\n" + + "6hGUvHcCBSDKP3fRz+scyJ9zwMt7nFXK5A/k2YgAAQCn3Es+IhvePn3eBlcYMMr0\n" + + "xcktrY1NJAIZPfjlUJ0J1g6LiNUEGBYKAH0FAmNbxHMCngECmwIFFgIDAQAECwkI\n" + + "BwUVCgkIC18gBBkWCgAGBQJjW8RzAAoJECxLf7KoUc8wD18BANNpIr4E+RRVVztR\n" + + "OVwdxSe0SRWGjkW8nHrRyghHKTuMAP9p4ZKicOYA1uZbiNNjyuJuS8xBH6Hihurb\n" + + "gDypVgxdBQAKCRDsaXwpK+RE4EQjAP9ARZEPxKNLFkrvjoZ8nrts3qhv3VtMrU+9\n" + + "huZnYLe1FQEAtgO6V7wutHvVARHXqPJ6lcv+SueIu+BjLFYEKuBwggs=\n" + + "=ShJd\n" + + "-----END PGP PRIVATE KEY BLOCK-----"; + + private static final String MSG = "-----BEGIN PGP MESSAGE-----\n" + + "Version: PGPainless\n" + + "\n" + + "hF4DJmQMTBqw3G8SAQdALkHpO0UkS/CqkwxUz74MJU3PV72ZrIL8ZcrO8ofhblkw\n" + + "iDIhSwwGTG3tj+sG+ZVWKsmONKi7Om5seJDHQtQ8MfdCELAgwYHSt6MrgDBhuDIH\n" + + "0kABZhq2/8qk3EGXPpc+xxs4r4g8SgHOiiHSim5NGtounXXIaF6T/hUmlorkeYf/\n" + + "a9pCC0QXRUAr8NOcdsfbvb5V\n" + + "=dQa8\n" + + "-----END PGP MESSAGE-----"; + + @Test + public void test() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, IOException { + PGPSecretKeyRing secretKeys = PGPainless.readKeyRing().secretKeyRing(KEY); + SecretKeyRingProtector protector = SecretKeyRingProtector.unprotectedKeys(); + KeyRingInfo info = PGPainless.inspectKeyRing(secretKeys); + SubkeyIdentifier decryptionKey = new SubkeyIdentifier(secretKeys, + info.getEncryptionSubkeys(EncryptionPurpose.ANY).get(0).getKeyID()); + + PGPSecretKey secretKey = secretKeys.getSecretKey(decryptionKey.getSubkeyId()); + PGPPrivateKey privateKey = UnlockSecretKey.unlockSecretKey(secretKey, protector); + CachingBcPublicKeyDataDecryptorFactory cachingFactory = new CachingBcPublicKeyDataDecryptorFactory( + privateKey, decryptionKey); + + ByteArrayInputStream ciphertextIn = new ByteArrayInputStream(MSG.getBytes()); + DecryptionStream decryptionStream = PGPainless.decryptAndOrVerify() + .onInputStream(ciphertextIn) + .withOptions(ConsumerOptions.get() + .addCustomDecryptorFactory(cachingFactory)); + + ByteArrayOutputStream out = new ByteArrayOutputStream(); + Streams.pipeAll(decryptionStream, out); + decryptionStream.close(); + + decryptionStream = PGPainless.decryptAndOrVerify() + .onInputStream(ciphertextIn) + .withOptions(ConsumerOptions.get() + .addCustomDecryptorFactory(cachingFactory)); + out = new ByteArrayOutputStream(); + Streams.pipeAll(decryptionStream, out); + decryptionStream.close(); + } +} From 4b8367dcca331083dec34ebb026f4f007d415496 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Fri, 28 Oct 2022 14:56:41 +0200 Subject: [PATCH 67/80] Fix HardwareSecurity.getIdsOfHardwareBackedKeys() --- .../decryption_verification/HardwareSecurity.java | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/HardwareSecurity.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/HardwareSecurity.java index f234ff00..6d9719dd 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/HardwareSecurity.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/HardwareSecurity.java @@ -47,8 +47,8 @@ public class HardwareSecurity { * @param secretKeys secret keys * @return set of keys with S2K type DIVERT_TO_CARD or GNU_DUMMY_S2K */ - public static Set getIdsOfHardwareBackedKeys(PGPSecretKeyRing secretKeys) { - Set hardwareBackedKeys = new HashSet<>(); + public static Set getIdsOfHardwareBackedKeys(PGPSecretKeyRing secretKeys) { + Set hardwareBackedKeys = new HashSet<>(); for (PGPSecretKey secretKey : secretKeys) { S2K s2K = secretKey.getS2K(); if (s2K == null) { @@ -56,9 +56,11 @@ public class HardwareSecurity { } int type = s2K.getType(); + int mode = s2K.getProtectionMode(); // TODO: Is GNU_DUMMY_S2K appropriate? - if (type == S2K.GNU_PROTECTION_MODE_DIVERT_TO_CARD || type == S2K.GNU_DUMMY_S2K) { - hardwareBackedKeys.add(secretKey.getKeyID()); + if (type == S2K.GNU_DUMMY_S2K && mode == S2K.GNU_PROTECTION_MODE_DIVERT_TO_CARD) { + SubkeyIdentifier hardwareBackedKey = new SubkeyIdentifier(secretKeys, secretKey.getKeyID()); + hardwareBackedKeys.add(hardwareBackedKey); } } return hardwareBackedKeys; @@ -75,7 +77,7 @@ public class HardwareSecurity { // luckily we can instantiate the BcPublicKeyDataDecryptorFactory with null as argument. private final PublicKeyDataDecryptorFactory factory = new BcPublicKeyDataDecryptorFactory(null); - private SubkeyIdentifier subkey; + private final SubkeyIdentifier subkey; /** * Create a new {@link HardwareDataDecryptorFactory}. From 220f186336eb337cc88753212f8781855c65d6b2 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Fri, 28 Oct 2022 16:20:36 +0200 Subject: [PATCH 68/80] Move CachingBcPublicKeyDataDecryptorFactoryTest to correct package --- .../CachingBcPublicKeyDataDecryptorFactoryTest.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) rename pgpainless-core/src/test/java/{ => org}/bouncycastle/CachingBcPublicKeyDataDecryptorFactoryTest.java (98%) diff --git a/pgpainless-core/src/test/java/bouncycastle/CachingBcPublicKeyDataDecryptorFactoryTest.java b/pgpainless-core/src/test/java/org/bouncycastle/CachingBcPublicKeyDataDecryptorFactoryTest.java similarity index 98% rename from pgpainless-core/src/test/java/bouncycastle/CachingBcPublicKeyDataDecryptorFactoryTest.java rename to pgpainless-core/src/test/java/org/bouncycastle/CachingBcPublicKeyDataDecryptorFactoryTest.java index 186bcbc2..6a43772d 100644 --- a/pgpainless-core/src/test/java/bouncycastle/CachingBcPublicKeyDataDecryptorFactoryTest.java +++ b/pgpainless-core/src/test/java/org/bouncycastle/CachingBcPublicKeyDataDecryptorFactoryTest.java @@ -2,7 +2,7 @@ // // SPDX-License-Identifier: Apache-2.0 -package bouncycastle; +package org.bouncycastle; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; @@ -10,7 +10,6 @@ import java.io.IOException; import java.security.InvalidAlgorithmParameterException; import java.security.NoSuchAlgorithmException; -import org.bouncycastle.CachingBcPublicKeyDataDecryptorFactory; import org.bouncycastle.openpgp.PGPException; import org.bouncycastle.openpgp.PGPPrivateKey; import org.bouncycastle.openpgp.PGPSecretKey; From 489419459a07634241396196ddb22edd0771153d Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Fri, 28 Oct 2022 16:36:08 +0200 Subject: [PATCH 69/80] Add and test GnuDummyKeyUtil --- .../key/gnu_dummy_s2k/GNUExtension.java | 24 +++ .../key/gnu_dummy_s2k/GnuDummyKeyUtil.java | 114 ++++++++++ .../key/gnu_dummy_s2k/package-info.java | 8 + .../HardwareSecurityTest.java | 82 ++++++++ .../gnu_dummy_s2k/GnuDummyKeyUtilTest.java | 199 ++++++++++++++++++ 5 files changed, 427 insertions(+) create mode 100644 pgpainless-core/src/main/java/org/pgpainless/key/gnu_dummy_s2k/GNUExtension.java create mode 100644 pgpainless-core/src/main/java/org/pgpainless/key/gnu_dummy_s2k/GnuDummyKeyUtil.java create mode 100644 pgpainless-core/src/main/java/org/pgpainless/key/gnu_dummy_s2k/package-info.java create mode 100644 pgpainless-core/src/test/java/org/pgpainless/decryption_verification/HardwareSecurityTest.java create mode 100644 pgpainless-core/src/test/java/org/pgpainless/key/gnu_dummy_s2k/GnuDummyKeyUtilTest.java diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/gnu_dummy_s2k/GNUExtension.java b/pgpainless-core/src/main/java/org/pgpainless/key/gnu_dummy_s2k/GNUExtension.java new file mode 100644 index 00000000..ff42ef6e --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/key/gnu_dummy_s2k/GNUExtension.java @@ -0,0 +1,24 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.key.gnu_dummy_s2k; + +import org.bouncycastle.bcpg.S2K; + +public enum GNUExtension { + + NO_PRIVATE_KEY(S2K.GNU_PROTECTION_MODE_NO_PRIVATE_KEY), + DIVERT_TO_CARD(S2K.GNU_PROTECTION_MODE_DIVERT_TO_CARD), + ; + + private final int id; + + GNUExtension(int id) { + this.id = id; + } + + public int getId() { + return id; + } +} diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/gnu_dummy_s2k/GnuDummyKeyUtil.java b/pgpainless-core/src/main/java/org/pgpainless/key/gnu_dummy_s2k/GnuDummyKeyUtil.java new file mode 100644 index 00000000..a8de5aa3 --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/key/gnu_dummy_s2k/GnuDummyKeyUtil.java @@ -0,0 +1,114 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.key.gnu_dummy_s2k; + +import org.bouncycastle.bcpg.PublicKeyPacket; +import org.bouncycastle.bcpg.S2K; +import org.bouncycastle.bcpg.SecretKeyPacket; +import org.bouncycastle.bcpg.SecretSubkeyPacket; +import org.bouncycastle.openpgp.PGPSecretKey; +import org.bouncycastle.openpgp.PGPSecretKeyRing; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +public final class GnuDummyKeyUtil { + + private GnuDummyKeyUtil() { + + } + + public static Builder modify(PGPSecretKeyRing secretKeys) { + return new Builder(secretKeys); + } + + public static final class Builder { + + private final PGPSecretKeyRing keys; + + private Builder(PGPSecretKeyRing keys) { + this.keys = keys; + } + + public PGPSecretKeyRing removePrivateKeys(KeyFilter filter) { + return doIt(GNUExtension.NO_PRIVATE_KEY, null, filter); + } + + public PGPSecretKeyRing divertPrivateKeysToCard(KeyFilter filter) { + return divertPrivateKeysToCard(filter, new byte[16]); + } + + public PGPSecretKeyRing divertPrivateKeysToCard(KeyFilter filter, byte[] cardSerialNumber) { + return doIt(GNUExtension.DIVERT_TO_CARD, cardSerialNumber, filter); + } + + private PGPSecretKeyRing doIt(GNUExtension extension, byte[] serial, KeyFilter filter) { + byte[] encodedSerial = serial != null ? encodeSerial(serial) : null; + S2K s2k = extensionToS2K(extension); + + List secretKeyList = new ArrayList<>(); + for (PGPSecretKey secretKey : keys) { + if (!filter.filter(secretKey.getKeyID())) { + // No conversion, do not modify subkey + secretKeyList.add(secretKey); + continue; + } + + PublicKeyPacket publicKeyPacket = secretKey.getPublicKey().getPublicKeyPacket(); + if (secretKey.isMasterKey()) { + SecretKeyPacket keyPacket = new SecretKeyPacket(publicKeyPacket, + 0, 255, s2k, null, encodedSerial); + PGPSecretKey onCard = new PGPSecretKey(keyPacket, secretKey.getPublicKey()); + secretKeyList.add(onCard); + } else { + SecretSubkeyPacket keyPacket = new SecretSubkeyPacket(publicKeyPacket, + 0, 255, s2k, null, encodedSerial); + PGPSecretKey onCard = new PGPSecretKey(keyPacket, secretKey.getPublicKey()); + secretKeyList.add(onCard); + } + } + + PGPSecretKeyRing gnuDummyKey = new PGPSecretKeyRing(secretKeyList); + return gnuDummyKey; + } + + private byte[] encodeSerial(byte[] serial) { + byte[] encoded = new byte[serial.length + 1]; + encoded[0] = 0x10; + System.arraycopy(serial, 0, encoded, 1, serial.length); + return encoded; + } + + private S2K extensionToS2K(GNUExtension extension) { + S2K s2k = S2K.gnuDummyS2K(extension == GNUExtension.DIVERT_TO_CARD ? + S2K.GNUDummyParams.divertToCard() : S2K.GNUDummyParams.noPrivateKey()); + return s2k; + } + } + + public interface KeyFilter { + + /** + * Return true, if the given key should be selected, false otherwise. + * + * @param keyId id of the key + * @return select + */ + boolean filter(long keyId); + + static KeyFilter any() { + return keyId -> true; + } + + static KeyFilter only(long onlyKeyId) { + return keyId -> keyId == onlyKeyId; + } + + static KeyFilter selected(Collection ids) { + return keyId -> ids.contains(keyId); + } + } +} diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/gnu_dummy_s2k/package-info.java b/pgpainless-core/src/main/java/org/pgpainless/key/gnu_dummy_s2k/package-info.java new file mode 100644 index 00000000..5c2a727e --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/key/gnu_dummy_s2k/package-info.java @@ -0,0 +1,8 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +/** + * Utility classes related to creating keys with GNU DUMMY S2K values. + */ +package org.pgpainless.key.gnu_dummy_s2k; diff --git a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/HardwareSecurityTest.java b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/HardwareSecurityTest.java new file mode 100644 index 00000000..f1606cec --- /dev/null +++ b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/HardwareSecurityTest.java @@ -0,0 +1,82 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.decryption_verification; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.IOException; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; + +import org.bouncycastle.openpgp.PGPSecretKey; +import org.bouncycastle.openpgp.PGPSecretKeyRing; +import org.junit.jupiter.api.Test; +import org.pgpainless.PGPainless; +import org.pgpainless.key.SubkeyIdentifier; +import org.pgpainless.key.gnu_dummy_s2k.GnuDummyKeyUtil; +import org.pgpainless.key.util.KeyIdUtil; + +public class HardwareSecurityTest { + + private static final String KEY = "-----BEGIN PGP PRIVATE KEY BLOCK-----\n" + + "Version: PGPainless\n" + + "Comment: DE2E 9AB2 6650 8191 53E7 D599 C176 507F 2B5D 43B3\n" + + "Comment: Alice \n" + + "\n" + + "lFgEY1vjgRYJKwYBBAHaRw8BAQdAXjLoPTOIOdvlFT2Nt3rcvLTVx5ujPBGghZ5S\n" + + "D5tEnyoAAP0fAUJTiPrxZYdzs6MP0KFo+Nmr/wb1PJHTkzmYpt4wkRKBtBxBbGlj\n" + + "ZSA8YWxpY2VAcGdwYWlubGVzcy5vcmc+iI8EExYKAEEFAmNb44EJEMF2UH8rXUOz\n" + + "FiEE3i6asmZQgZFT59WZwXZQfytdQ7MCngECmwEFFgIDAQAECwkIBwUVCgkICwKZ\n" + + "AQAAHLYA/AgW+YrpU+UqrwX2dhY6RAfgHTTMU89RHjaTHJx8pLrBAP4gthGof00a\n" + + "XEjwTWteDOO049SIp2AUfj9deJqtrQcHD5xdBGNb44ESCisGAQQBl1UBBQEBB0DN\n" + + "vUT3awa3YLmwf41LRpPrm7B87AOHfYIP8S9QJ4GDJgMBCAcAAP9bwlSaF+lti8JY\n" + + "qKFO3qt3ZYQMu1l/LRBle89ZB4zD+BDOiHUEGBYKAB0FAmNb44ECngECmwwFFgID\n" + + "AQAECwkIBwUVCgkICwAKCRDBdlB/K11Ds/TsAP9kvpUrCWnrWGq+a9n1CqEfCMX5\n" + + "cT+qzrwNf+J0L22KowD+M9SVO0qssiAqutLE9h9dGYLbEiFvsHzK3WSnjKYbIgac\n" + + "WARjW+OBFgkrBgEEAdpHDwEBB0BCPh8M5TnXSmG6Ygwp4j5RR4u3hmxl8CYjX4h/\n" + + "XtvvNwAA/RP04coSrLHVI6vUfbJk4MhWYeyhJBRYY0vGp7yq+wVtEpKI1QQYFgoA\n" + + "fQUCY1vjgQKeAQKbAgUWAgMBAAQLCQgHBRUKCQgLXyAEGRYKAAYFAmNb44EACgkQ\n" + + "mlozJSF7rXQW+AD/TA3YBxTd+YbBSwfgqzWNbfT9BBcFrdn3uPCsbvfmqXoA/3oj\n" + + "oupkgoaXesrGxn2k9hW9/GBXSvNcgY2txZ6/oYoIAAoJEMF2UH8rXUOziZ4A/0Xl\n" + + "xSZJWmkRpBh5AO8Cnqosz6j947IYAxS16ay+sIOHAP9aN9CUNJIIdHnHdFHO4GZz\n" + + "ejjknn4wt8NVJP97JxlnBQ==\n" + + "=qSQb\n" + + "-----END PGP PRIVATE KEY BLOCK-----"; + + @Test + public void testGetSingleIdOfHardwareBackedKey() throws IOException { + PGPSecretKeyRing secretKeys = PGPainless.readKeyRing().secretKeyRing(KEY); + assertTrue(HardwareSecurity.getIdsOfHardwareBackedKeys(secretKeys).isEmpty()); + long encryptionKeyId = KeyIdUtil.fromLongKeyId("0AAD8F5891262F50"); + + PGPSecretKeyRing withHardwareBackedEncryptionKey = GnuDummyKeyUtil.modify(secretKeys) + .divertPrivateKeysToCard(GnuDummyKeyUtil.KeyFilter.only(encryptionKeyId)); + + Set hardwareBackedKeys = HardwareSecurity + .getIdsOfHardwareBackedKeys(withHardwareBackedEncryptionKey); + assertEquals(Collections.singleton(new SubkeyIdentifier(secretKeys, encryptionKeyId)), hardwareBackedKeys); + } + + + @Test + public void testGetIdsOfFullyHardwareBackedKey() throws IOException { + PGPSecretKeyRing secretKeys = PGPainless.readKeyRing().secretKeyRing(KEY); + assertTrue(HardwareSecurity.getIdsOfHardwareBackedKeys(secretKeys).isEmpty()); + + PGPSecretKeyRing withHardwareBackedEncryptionKey = GnuDummyKeyUtil.modify(secretKeys) + .divertPrivateKeysToCard(GnuDummyKeyUtil.KeyFilter.any()); + Set expected = new HashSet<>(); + for (PGPSecretKey key : secretKeys) { + expected.add(new SubkeyIdentifier(secretKeys, key.getKeyID())); + } + + Set hardwareBackedKeys = HardwareSecurity + .getIdsOfHardwareBackedKeys(withHardwareBackedEncryptionKey); + + assertEquals(expected, hardwareBackedKeys); + } +} diff --git a/pgpainless-core/src/test/java/org/pgpainless/key/gnu_dummy_s2k/GnuDummyKeyUtilTest.java b/pgpainless-core/src/test/java/org/pgpainless/key/gnu_dummy_s2k/GnuDummyKeyUtilTest.java new file mode 100644 index 00000000..0b19c551 --- /dev/null +++ b/pgpainless-core/src/test/java/org/pgpainless/key/gnu_dummy_s2k/GnuDummyKeyUtilTest.java @@ -0,0 +1,199 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.key.gnu_dummy_s2k; + +import org.bouncycastle.bcpg.S2K; +import org.bouncycastle.openpgp.PGPSecretKey; +import org.bouncycastle.openpgp.PGPSecretKeyRing; +import org.junit.jupiter.api.Test; +import org.pgpainless.PGPainless; +import org.pgpainless.key.util.KeyIdUtil; + +import java.io.IOException; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class GnuDummyKeyUtilTest { + // normal, non-hw-backed key + private static final String FULL_KEY = "-----BEGIN PGP PRIVATE KEY BLOCK-----\n" + + "Version: PGPainless\n" + + "Comment: 01FD AB6C E04A 5078 79FE 4A18 C312 C97D A9F7 6A4F\n" + + "Comment: Hardy Hardware \n" + + "\n" + + "lFgEY1vSiBYJKwYBBAHaRw8BAQdAQ58lZn/HOtg+1b1KS18odyQ6M4LaDdbJAyRf\n" + + "eBwCeTQAAPwJN+Xmr0jjN7RA9jgqXnxC/rcWHmdp/j9NdEd7K2Wbxw/rtCBIYXJk\n" + + "eSBIYXJkd2FyZSA8aGFyZHlAaGFyZC53YXJlPoiPBBMWCgBBBQJjW9KICRDDEsl9\n" + + "qfdqTxYhBAH9q2zgSlB4ef5KGMMSyX2p92pPAp4BApsBBRYCAwEABAsJCAcFFQoJ\n" + + "CAsCmQEAAPk2AP922T5TQ7hukFlpxX3ThMhieJnECGY5Eqt5U0/vEY1XdgD/eE1M\n" + + "l9qqx6QGcaNKe8deMe3EhTant6mS9tqMHp2/3gmcXQRjW9KIEgorBgEEAZdVAQUB\n" + + "AQdAVXBLNvNmFh9KX6iLmdNJM28Zc9PGnzEoAD9+T4p0lDwDAQgHAAD/fw9hnzeH\n" + + "VtBaHi6efXvnc4rdVj8zWk0LKo1clFd3bTAN+oh1BBgWCgAdBQJjW9KIAp4BApsM\n" + + "BRYCAwEABAsJCAcFFQoJCAsACgkQwxLJfan3ak/JyQD9GBj0vjtYZAf5Fi0eEKdi\n" + + "Ags0yZrQPkMs6eL+83te770A/jG0DeJy+88fOfWTj+mixO98PZPnQ0MybWC/1QUT\n" + + "vP0BnFgEY1vSiBYJKwYBBAHaRw8BAQdAvSYTD60t8vx10dSEBACUoIfVCpeOB30D\n" + + "6nfwJtbDT0YAAQCgnCsN9iX7s2TQd8NPggWs4QdhaFpb6olt3SlAvUy/wRBDiNUE\n" + + "GBYKAH0FAmNb0ogCngECmwIFFgIDAQAECwkIBwUVCgkIC18gBBkWCgAGBQJjW9KI\n" + + "AAoJEJQCL6VtwFtJDmMBAKqsGfRFQxJXyPgugWBgEaO5lt9fMM0yUxa76cmSWe5f\n" + + "AQD2oLSEW1GOgIs64+Z3gvtXopmeupT09HhI7ger98zDAwAKCRDDEsl9qfdqTwR6\n" + + "AP9Xftw8xZ7/MWhYImk/xheqPy07K4qo3T1pGKUvUqjWQQEAhE3r0oTcJn+KVCwG\n" + + "jF6AYiLOzO/R1x5bSlYD3FeJ3Qo=\n" + + "=+vXp\n" + + "-----END PGP PRIVATE KEY BLOCK-----\n"; + + private static final long primaryKeyId = KeyIdUtil.fromLongKeyId("C312C97DA9F76A4F"); + private static final long encryptionKeyId = KeyIdUtil.fromLongKeyId("6924D066714CE8C6"); + private static final long signatureKeyId = KeyIdUtil.fromLongKeyId("94022FA56DC05B49"); + private static final byte[] cardSerial = new byte[] {0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f}; + + public static final String ALL_KEYS_ON_CARD = "-----BEGIN PGP PRIVATE KEY BLOCK-----\n" + + "Version: PGPainless\n" + + "Comment: 01FD AB6C E04A 5078 79FE 4A18 C312 C97D A9F7 6A4F\n" + + "Comment: Hardy Hardware \n" + + "\n" + + "lEwEY1vSiBYJKwYBBAHaRw8BAQdAQ58lZn/HOtg+1b1KS18odyQ6M4LaDdbJAyRf\n" + + "eBwCeTT/AGUAR05VAhAAAQIDBAUGBwgJCgsMDQ4PtCBIYXJkeSBIYXJkd2FyZSA8\n" + + "aGFyZHlAaGFyZC53YXJlPoiPBBMWCgBBBQJjW9KICRDDEsl9qfdqTxYhBAH9q2zg\n" + + "SlB4ef5KGMMSyX2p92pPAp4BApsBBRYCAwEABAsJCAcFFQoJCAsCmQEAAPk2AP92\n" + + "2T5TQ7hukFlpxX3ThMhieJnECGY5Eqt5U0/vEY1XdgD/eE1Ml9qqx6QGcaNKe8de\n" + + "Me3EhTant6mS9tqMHp2/3gmcUQRjW9KIEgorBgEEAZdVAQUBAQdAVXBLNvNmFh9K\n" + + "X6iLmdNJM28Zc9PGnzEoAD9+T4p0lDwDAQgH/wBlAEdOVQIQAAECAwQFBgcICQoL\n" + + "DA0OD4h1BBgWCgAdBQJjW9KIAp4BApsMBRYCAwEABAsJCAcFFQoJCAsACgkQwxLJ\n" + + "fan3ak/JyQD9GBj0vjtYZAf5Fi0eEKdiAgs0yZrQPkMs6eL+83te770A/jG0DeJy\n" + + "+88fOfWTj+mixO98PZPnQ0MybWC/1QUTvP0BnEwEY1vSiBYJKwYBBAHaRw8BAQdA\n" + + "vSYTD60t8vx10dSEBACUoIfVCpeOB30D6nfwJtbDT0b/AGUAR05VAhAAAQIDBAUG\n" + + "BwgJCgsMDQ4PiNUEGBYKAH0FAmNb0ogCngECmwIFFgIDAQAECwkIBwUVCgkIC18g\n" + + "BBkWCgAGBQJjW9KIAAoJEJQCL6VtwFtJDmMBAKqsGfRFQxJXyPgugWBgEaO5lt9f\n" + + "MM0yUxa76cmSWe5fAQD2oLSEW1GOgIs64+Z3gvtXopmeupT09HhI7ger98zDAwAK\n" + + "CRDDEsl9qfdqTwR6AP9Xftw8xZ7/MWhYImk/xheqPy07K4qo3T1pGKUvUqjWQQEA\n" + + "hE3r0oTcJn+KVCwGjF6AYiLOzO/R1x5bSlYD3FeJ3Qo=\n" + + "=wsFa\n" + + "-----END PGP PRIVATE KEY BLOCK-----"; + + public static final String PRIMARY_KEY_ON_CARD = "-----BEGIN PGP PRIVATE KEY BLOCK-----\n" + + "Version: PGPainless\n" + + "Comment: 01FD AB6C E04A 5078 79FE 4A18 C312 C97D A9F7 6A4F\n" + + "Comment: Hardy Hardware \n" + + "\n" + + "lEwEY1vSiBYJKwYBBAHaRw8BAQdAQ58lZn/HOtg+1b1KS18odyQ6M4LaDdbJAyRf\n" + + "eBwCeTT/AGUAR05VAhAAAQIDBAUGBwgJCgsMDQ4PtCBIYXJkeSBIYXJkd2FyZSA8\n" + + "aGFyZHlAaGFyZC53YXJlPoiPBBMWCgBBBQJjW9KICRDDEsl9qfdqTxYhBAH9q2zg\n" + + "SlB4ef5KGMMSyX2p92pPAp4BApsBBRYCAwEABAsJCAcFFQoJCAsCmQEAAPk2AP92\n" + + "2T5TQ7hukFlpxX3ThMhieJnECGY5Eqt5U0/vEY1XdgD/eE1Ml9qqx6QGcaNKe8de\n" + + "Me3EhTant6mS9tqMHp2/3gmcXQRjW9KIEgorBgEEAZdVAQUBAQdAVXBLNvNmFh9K\n" + + "X6iLmdNJM28Zc9PGnzEoAD9+T4p0lDwDAQgHAAD/fw9hnzeHVtBaHi6efXvnc4rd\n" + + "Vj8zWk0LKo1clFd3bTAN+oh1BBgWCgAdBQJjW9KIAp4BApsMBRYCAwEABAsJCAcF\n" + + "FQoJCAsACgkQwxLJfan3ak/JyQD9GBj0vjtYZAf5Fi0eEKdiAgs0yZrQPkMs6eL+\n" + + "83te770A/jG0DeJy+88fOfWTj+mixO98PZPnQ0MybWC/1QUTvP0BnFgEY1vSiBYJ\n" + + "KwYBBAHaRw8BAQdAvSYTD60t8vx10dSEBACUoIfVCpeOB30D6nfwJtbDT0YAAQCg\n" + + "nCsN9iX7s2TQd8NPggWs4QdhaFpb6olt3SlAvUy/wRBDiNUEGBYKAH0FAmNb0ogC\n" + + "ngECmwIFFgIDAQAECwkIBwUVCgkIC18gBBkWCgAGBQJjW9KIAAoJEJQCL6VtwFtJ\n" + + "DmMBAKqsGfRFQxJXyPgugWBgEaO5lt9fMM0yUxa76cmSWe5fAQD2oLSEW1GOgIs6\n" + + "4+Z3gvtXopmeupT09HhI7ger98zDAwAKCRDDEsl9qfdqTwR6AP9Xftw8xZ7/MWhY\n" + + "Imk/xheqPy07K4qo3T1pGKUvUqjWQQEAhE3r0oTcJn+KVCwGjF6AYiLOzO/R1x5b\n" + + "SlYD3FeJ3Qo=\n" + + "=s+B1\n" + + "-----END PGP PRIVATE KEY BLOCK-----"; + + public static final String ENCRYPTION_KEY_ON_CARD = "-----BEGIN PGP PRIVATE KEY BLOCK-----\n" + + "Version: PGPainless\n" + + "Comment: 01FD AB6C E04A 5078 79FE 4A18 C312 C97D A9F7 6A4F\n" + + "Comment: Hardy Hardware \n" + + "\n" + + "lFgEY1vSiBYJKwYBBAHaRw8BAQdAQ58lZn/HOtg+1b1KS18odyQ6M4LaDdbJAyRf\n" + + "eBwCeTQAAPwJN+Xmr0jjN7RA9jgqXnxC/rcWHmdp/j9NdEd7K2Wbxw/rtCBIYXJk\n" + + "eSBIYXJkd2FyZSA8aGFyZHlAaGFyZC53YXJlPoiPBBMWCgBBBQJjW9KICRDDEsl9\n" + + "qfdqTxYhBAH9q2zgSlB4ef5KGMMSyX2p92pPAp4BApsBBRYCAwEABAsJCAcFFQoJ\n" + + "CAsCmQEAAPk2AP922T5TQ7hukFlpxX3ThMhieJnECGY5Eqt5U0/vEY1XdgD/eE1M\n" + + "l9qqx6QGcaNKe8deMe3EhTant6mS9tqMHp2/3gmcUQRjW9KIEgorBgEEAZdVAQUB\n" + + "AQdAVXBLNvNmFh9KX6iLmdNJM28Zc9PGnzEoAD9+T4p0lDwDAQgH/wBlAEdOVQIQ\n" + + "AAECAwQFBgcICQoLDA0OD4h1BBgWCgAdBQJjW9KIAp4BApsMBRYCAwEABAsJCAcF\n" + + "FQoJCAsACgkQwxLJfan3ak/JyQD9GBj0vjtYZAf5Fi0eEKdiAgs0yZrQPkMs6eL+\n" + + "83te770A/jG0DeJy+88fOfWTj+mixO98PZPnQ0MybWC/1QUTvP0BnFgEY1vSiBYJ\n" + + "KwYBBAHaRw8BAQdAvSYTD60t8vx10dSEBACUoIfVCpeOB30D6nfwJtbDT0YAAQCg\n" + + "nCsN9iX7s2TQd8NPggWs4QdhaFpb6olt3SlAvUy/wRBDiNUEGBYKAH0FAmNb0ogC\n" + + "ngECmwIFFgIDAQAECwkIBwUVCgkIC18gBBkWCgAGBQJjW9KIAAoJEJQCL6VtwFtJ\n" + + "DmMBAKqsGfRFQxJXyPgugWBgEaO5lt9fMM0yUxa76cmSWe5fAQD2oLSEW1GOgIs6\n" + + "4+Z3gvtXopmeupT09HhI7ger98zDAwAKCRDDEsl9qfdqTwR6AP9Xftw8xZ7/MWhY\n" + + "Imk/xheqPy07K4qo3T1pGKUvUqjWQQEAhE3r0oTcJn+KVCwGjF6AYiLOzO/R1x5b\n" + + "SlYD3FeJ3Qo=\n" + + "=TPAl\n" + + "-----END PGP PRIVATE KEY BLOCK-----"; + + public static final String SIGNATURE_KEY_ON_CARD = "-----BEGIN PGP PRIVATE KEY BLOCK-----\n" + + "Version: PGPainless\n" + + "Comment: 01FD AB6C E04A 5078 79FE 4A18 C312 C97D A9F7 6A4F\n" + + "Comment: Hardy Hardware \n" + + "\n" + + "lFgEY1vSiBYJKwYBBAHaRw8BAQdAQ58lZn/HOtg+1b1KS18odyQ6M4LaDdbJAyRf\n" + + "eBwCeTQAAPwJN+Xmr0jjN7RA9jgqXnxC/rcWHmdp/j9NdEd7K2Wbxw/rtCBIYXJk\n" + + "eSBIYXJkd2FyZSA8aGFyZHlAaGFyZC53YXJlPoiPBBMWCgBBBQJjW9KICRDDEsl9\n" + + "qfdqTxYhBAH9q2zgSlB4ef5KGMMSyX2p92pPAp4BApsBBRYCAwEABAsJCAcFFQoJ\n" + + "CAsCmQEAAPk2AP922T5TQ7hukFlpxX3ThMhieJnECGY5Eqt5U0/vEY1XdgD/eE1M\n" + + "l9qqx6QGcaNKe8deMe3EhTant6mS9tqMHp2/3gmcXQRjW9KIEgorBgEEAZdVAQUB\n" + + "AQdAVXBLNvNmFh9KX6iLmdNJM28Zc9PGnzEoAD9+T4p0lDwDAQgHAAD/fw9hnzeH\n" + + "VtBaHi6efXvnc4rdVj8zWk0LKo1clFd3bTAN+oh1BBgWCgAdBQJjW9KIAp4BApsM\n" + + "BRYCAwEABAsJCAcFFQoJCAsACgkQwxLJfan3ak/JyQD9GBj0vjtYZAf5Fi0eEKdi\n" + + "Ags0yZrQPkMs6eL+83te770A/jG0DeJy+88fOfWTj+mixO98PZPnQ0MybWC/1QUT\n" + + "vP0BnEwEY1vSiBYJKwYBBAHaRw8BAQdAvSYTD60t8vx10dSEBACUoIfVCpeOB30D\n" + + "6nfwJtbDT0b/AGUAR05VAhAAAQIDBAUGBwgJCgsMDQ4PiNUEGBYKAH0FAmNb0ogC\n" + + "ngECmwIFFgIDAQAECwkIBwUVCgkIC18gBBkWCgAGBQJjW9KIAAoJEJQCL6VtwFtJ\n" + + "DmMBAKqsGfRFQxJXyPgugWBgEaO5lt9fMM0yUxa76cmSWe5fAQD2oLSEW1GOgIs6\n" + + "4+Z3gvtXopmeupT09HhI7ger98zDAwAKCRDDEsl9qfdqTwR6AP9Xftw8xZ7/MWhY\n" + + "Imk/xheqPy07K4qo3T1pGKUvUqjWQQEAhE3r0oTcJn+KVCwGjF6AYiLOzO/R1x5b\n" + + "SlYD3FeJ3Qo=\n" + + "=p8I9\n" + + "-----END PGP PRIVATE KEY BLOCK-----"; + + @Test + public void testMoveAllKeysToCard() throws IOException { + PGPSecretKeyRing secretKeys = PGPainless.readKeyRing().secretKeyRing(FULL_KEY); + PGPSecretKeyRing expected = PGPainless.readKeyRing().secretKeyRing(ALL_KEYS_ON_CARD); + + PGPSecretKeyRing onCard = GnuDummyKeyUtil.modify(secretKeys) + .divertPrivateKeysToCard(GnuDummyKeyUtil.KeyFilter.any(), cardSerial); + + for (PGPSecretKey key : onCard) { + assertEquals(255, key.getS2KUsage()); + S2K s2K = key.getS2K(); + assertEquals(S2K.GNU_PROTECTION_MODE_DIVERT_TO_CARD, s2K.getProtectionMode()); + } + + assertArrayEquals(expected.getEncoded(), onCard.getEncoded()); + } + + @Test + public void testMovePrimaryKeyToCard() throws IOException { + PGPSecretKeyRing secretKeys = PGPainless.readKeyRing().secretKeyRing(FULL_KEY); + PGPSecretKeyRing expected = PGPainless.readKeyRing().secretKeyRing(PRIMARY_KEY_ON_CARD); + + PGPSecretKeyRing onCard = GnuDummyKeyUtil.modify(secretKeys) + .divertPrivateKeysToCard(GnuDummyKeyUtil.KeyFilter.only(primaryKeyId), cardSerial); + + assertArrayEquals(expected.getEncoded(), onCard.getEncoded()); + } + + @Test + public void testMoveEncryptionKeyToCard() throws IOException { + PGPSecretKeyRing secretKeys = PGPainless.readKeyRing().secretKeyRing(FULL_KEY); + PGPSecretKeyRing expected = PGPainless.readKeyRing().secretKeyRing(ENCRYPTION_KEY_ON_CARD); + + PGPSecretKeyRing onCard = GnuDummyKeyUtil.modify(secretKeys) + .divertPrivateKeysToCard(GnuDummyKeyUtil.KeyFilter.only(encryptionKeyId), cardSerial); + + assertArrayEquals(expected.getEncoded(), onCard.getEncoded()); + } + + @Test + public void testMoveSigningKeyToCard() throws IOException { + PGPSecretKeyRing secretKeys = PGPainless.readKeyRing().secretKeyRing(FULL_KEY); + PGPSecretKeyRing expected = PGPainless.readKeyRing().secretKeyRing(SIGNATURE_KEY_ON_CARD); + + PGPSecretKeyRing onCard = GnuDummyKeyUtil.modify(secretKeys) + .divertPrivateKeysToCard(GnuDummyKeyUtil.KeyFilter.only(signatureKeyId), cardSerial); + + assertArrayEquals(expected.getEncoded(), onCard.getEncoded()); + } +} From 54ed7a5387dfc57ef0603151f93961bc31ee6279 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Fri, 28 Oct 2022 16:48:49 +0200 Subject: [PATCH 70/80] Add documentation to GnuDummyKeyUtil --- .../key/gnu_dummy_s2k/GnuDummyKeyUtil.java | 76 ++++++++++++++++--- 1 file changed, 66 insertions(+), 10 deletions(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/gnu_dummy_s2k/GnuDummyKeyUtil.java b/pgpainless-core/src/main/java/org/pgpainless/key/gnu_dummy_s2k/GnuDummyKeyUtil.java index a8de5aa3..f074fad2 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/key/gnu_dummy_s2k/GnuDummyKeyUtil.java +++ b/pgpainless-core/src/main/java/org/pgpainless/key/gnu_dummy_s2k/GnuDummyKeyUtil.java @@ -11,16 +11,26 @@ import org.bouncycastle.bcpg.SecretSubkeyPacket; import org.bouncycastle.openpgp.PGPSecretKey; import org.bouncycastle.openpgp.PGPSecretKeyRing; +import javax.annotation.Nonnull; import java.util.ArrayList; import java.util.Collection; import java.util.List; +/** + * This class can be used to remove private keys from secret keys. + */ public final class GnuDummyKeyUtil { private GnuDummyKeyUtil() { } + /** + * Modify the given {@link PGPSecretKeyRing}. + * + * @param secretKeys secret keys + * @return builder + */ public static Builder modify(PGPSecretKeyRing secretKeys) { return new Builder(secretKeys); } @@ -33,19 +43,50 @@ public final class GnuDummyKeyUtil { this.keys = keys; } + /** + * Remove all private keys that match the given {@link KeyFilter} from the key ring and replace them with + * GNU_DUMMY keys with S2K protection mode {@link GNUExtension#NO_PRIVATE_KEY}. + * + * @param filter filter to select keys for removal + * @return modified key ring + */ public PGPSecretKeyRing removePrivateKeys(KeyFilter filter) { - return doIt(GNUExtension.NO_PRIVATE_KEY, null, filter); + return replacePrivateKeys(GNUExtension.NO_PRIVATE_KEY, null, filter); } + /** + * Remove all private keys that match the given {@link KeyFilter} from the key ring and replace them with + * GNU_DUMMY keys with S2K protection mode {@link GNUExtension#DIVERT_TO_CARD}. + * This method will set the serial number of the card to 0x00000000000000000000000000000000. + * + * NOTE: This method does not actually move any keys to a card. + * + * @param filter filter to select keys for removal + * @return modified key ring + */ public PGPSecretKeyRing divertPrivateKeysToCard(KeyFilter filter) { return divertPrivateKeysToCard(filter, new byte[16]); } + /** + * Remove all private keys that match the given {@link KeyFilter} from the key ring and replace them with + * GNU_DUMMY keys with S2K protection mode {@link GNUExtension#DIVERT_TO_CARD}. + * This method will include the card serial number into the encoded dummy key. + * + * NOTE: This method does not actually move any keys to a card. + * + * @param filter filter to select keys for removal + * @param cardSerialNumber serial number of the card (at most 16 bytes long) + * @return modified key ring + */ public PGPSecretKeyRing divertPrivateKeysToCard(KeyFilter filter, byte[] cardSerialNumber) { - return doIt(GNUExtension.DIVERT_TO_CARD, cardSerialNumber, filter); + if (cardSerialNumber != null && cardSerialNumber.length > 16) { + throw new IllegalArgumentException("Card serial number length cannot exceed 16 bytes."); + } + return replacePrivateKeys(GNUExtension.DIVERT_TO_CARD, cardSerialNumber, filter); } - private PGPSecretKeyRing doIt(GNUExtension extension, byte[] serial, KeyFilter filter) { + private PGPSecretKeyRing replacePrivateKeys(GNUExtension extension, byte[] serial, KeyFilter filter) { byte[] encodedSerial = serial != null ? encodeSerial(serial) : null; S2K s2k = extensionToS2K(extension); @@ -71,21 +112,19 @@ public final class GnuDummyKeyUtil { } } - PGPSecretKeyRing gnuDummyKey = new PGPSecretKeyRing(secretKeyList); - return gnuDummyKey; + return new PGPSecretKeyRing(secretKeyList); } - private byte[] encodeSerial(byte[] serial) { + private byte[] encodeSerial(@Nonnull byte[] serial) { byte[] encoded = new byte[serial.length + 1]; - encoded[0] = 0x10; + encoded[0] = (byte) (serial.length & 0xff); System.arraycopy(serial, 0, encoded, 1, serial.length); return encoded; } - private S2K extensionToS2K(GNUExtension extension) { - S2K s2k = S2K.gnuDummyS2K(extension == GNUExtension.DIVERT_TO_CARD ? + private S2K extensionToS2K(@Nonnull GNUExtension extension) { + return S2K.gnuDummyS2K(extension == GNUExtension.DIVERT_TO_CARD ? S2K.GNUDummyParams.divertToCard() : S2K.GNUDummyParams.noPrivateKey()); - return s2k; } } @@ -99,15 +138,32 @@ public final class GnuDummyKeyUtil { */ boolean filter(long keyId); + /** + * Select any key. + * @return filter + */ static KeyFilter any() { return keyId -> true; } + /** + * Select only the given keyId. + * + * @param onlyKeyId only acceptable key id + * @return filter + */ static KeyFilter only(long onlyKeyId) { return keyId -> keyId == onlyKeyId; } + /** + * Select all keyIds which are contained in the given set of ids. + * + * @param ids set of acceptable keyIds + * @return filter + */ static KeyFilter selected(Collection ids) { + // noinspection Convert2MethodRef return keyId -> ids.contains(keyId); } } From b5bd4875ae8cae1fa54e718593d272530a3a74cf Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Fri, 28 Oct 2022 17:05:56 +0200 Subject: [PATCH 71/80] Use S2K usage SHA1 in GnuDummyKeyUtil --- .../key/gnu_dummy_s2k/GNUExtension.java | 7 ++++++ .../key/gnu_dummy_s2k/GnuDummyKeyUtil.java | 6 ++--- .../gnu_dummy_s2k/GnuDummyKeyUtilTest.java | 23 ++++++++++--------- 3 files changed, 22 insertions(+), 14 deletions(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/gnu_dummy_s2k/GNUExtension.java b/pgpainless-core/src/main/java/org/pgpainless/key/gnu_dummy_s2k/GNUExtension.java index ff42ef6e..e829bd7b 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/key/gnu_dummy_s2k/GNUExtension.java +++ b/pgpainless-core/src/main/java/org/pgpainless/key/gnu_dummy_s2k/GNUExtension.java @@ -8,7 +8,14 @@ import org.bouncycastle.bcpg.S2K; public enum GNUExtension { + /** + * Do not store the secret part at all. + */ NO_PRIVATE_KEY(S2K.GNU_PROTECTION_MODE_NO_PRIVATE_KEY), + + /** + * A stub to access smartcards. + */ DIVERT_TO_CARD(S2K.GNU_PROTECTION_MODE_DIVERT_TO_CARD), ; diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/gnu_dummy_s2k/GnuDummyKeyUtil.java b/pgpainless-core/src/main/java/org/pgpainless/key/gnu_dummy_s2k/GnuDummyKeyUtil.java index f074fad2..7817a676 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/key/gnu_dummy_s2k/GnuDummyKeyUtil.java +++ b/pgpainless-core/src/main/java/org/pgpainless/key/gnu_dummy_s2k/GnuDummyKeyUtil.java @@ -74,7 +74,7 @@ public final class GnuDummyKeyUtil { * This method will include the card serial number into the encoded dummy key. * * NOTE: This method does not actually move any keys to a card. - * + * * @param filter filter to select keys for removal * @param cardSerialNumber serial number of the card (at most 16 bytes long) * @return modified key ring @@ -101,12 +101,12 @@ public final class GnuDummyKeyUtil { PublicKeyPacket publicKeyPacket = secretKey.getPublicKey().getPublicKeyPacket(); if (secretKey.isMasterKey()) { SecretKeyPacket keyPacket = new SecretKeyPacket(publicKeyPacket, - 0, 255, s2k, null, encodedSerial); + 0, SecretKeyPacket.USAGE_SHA1, s2k, null, encodedSerial); PGPSecretKey onCard = new PGPSecretKey(keyPacket, secretKey.getPublicKey()); secretKeyList.add(onCard); } else { SecretSubkeyPacket keyPacket = new SecretSubkeyPacket(publicKeyPacket, - 0, 255, s2k, null, encodedSerial); + 0, SecretKeyPacket.USAGE_SHA1, s2k, null, encodedSerial); PGPSecretKey onCard = new PGPSecretKey(keyPacket, secretKey.getPublicKey()); secretKeyList.add(onCard); } diff --git a/pgpainless-core/src/test/java/org/pgpainless/key/gnu_dummy_s2k/GnuDummyKeyUtilTest.java b/pgpainless-core/src/test/java/org/pgpainless/key/gnu_dummy_s2k/GnuDummyKeyUtilTest.java index 0b19c551..99966903 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/key/gnu_dummy_s2k/GnuDummyKeyUtilTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/key/gnu_dummy_s2k/GnuDummyKeyUtilTest.java @@ -5,6 +5,7 @@ package org.pgpainless.key.gnu_dummy_s2k; import org.bouncycastle.bcpg.S2K; +import org.bouncycastle.bcpg.SecretKeyPacket; import org.bouncycastle.openpgp.PGPSecretKey; import org.bouncycastle.openpgp.PGPSecretKeyRing; import org.junit.jupiter.api.Test; @@ -54,22 +55,22 @@ public class GnuDummyKeyUtilTest { "Comment: Hardy Hardware \n" + "\n" + "lEwEY1vSiBYJKwYBBAHaRw8BAQdAQ58lZn/HOtg+1b1KS18odyQ6M4LaDdbJAyRf\n" + - "eBwCeTT/AGUAR05VAhAAAQIDBAUGBwgJCgsMDQ4PtCBIYXJkeSBIYXJkd2FyZSA8\n" + + "eBwCeTT+AGUAR05VAhAAAQIDBAUGBwgJCgsMDQ4PtCBIYXJkeSBIYXJkd2FyZSA8\n" + "aGFyZHlAaGFyZC53YXJlPoiPBBMWCgBBBQJjW9KICRDDEsl9qfdqTxYhBAH9q2zg\n" + "SlB4ef5KGMMSyX2p92pPAp4BApsBBRYCAwEABAsJCAcFFQoJCAsCmQEAAPk2AP92\n" + "2T5TQ7hukFlpxX3ThMhieJnECGY5Eqt5U0/vEY1XdgD/eE1Ml9qqx6QGcaNKe8de\n" + "Me3EhTant6mS9tqMHp2/3gmcUQRjW9KIEgorBgEEAZdVAQUBAQdAVXBLNvNmFh9K\n" + - "X6iLmdNJM28Zc9PGnzEoAD9+T4p0lDwDAQgH/wBlAEdOVQIQAAECAwQFBgcICQoL\n" + + "X6iLmdNJM28Zc9PGnzEoAD9+T4p0lDwDAQgH/gBlAEdOVQIQAAECAwQFBgcICQoL\n" + "DA0OD4h1BBgWCgAdBQJjW9KIAp4BApsMBRYCAwEABAsJCAcFFQoJCAsACgkQwxLJ\n" + "fan3ak/JyQD9GBj0vjtYZAf5Fi0eEKdiAgs0yZrQPkMs6eL+83te770A/jG0DeJy\n" + "+88fOfWTj+mixO98PZPnQ0MybWC/1QUTvP0BnEwEY1vSiBYJKwYBBAHaRw8BAQdA\n" + - "vSYTD60t8vx10dSEBACUoIfVCpeOB30D6nfwJtbDT0b/AGUAR05VAhAAAQIDBAUG\n" + + "vSYTD60t8vx10dSEBACUoIfVCpeOB30D6nfwJtbDT0b+AGUAR05VAhAAAQIDBAUG\n" + "BwgJCgsMDQ4PiNUEGBYKAH0FAmNb0ogCngECmwIFFgIDAQAECwkIBwUVCgkIC18g\n" + "BBkWCgAGBQJjW9KIAAoJEJQCL6VtwFtJDmMBAKqsGfRFQxJXyPgugWBgEaO5lt9f\n" + "MM0yUxa76cmSWe5fAQD2oLSEW1GOgIs64+Z3gvtXopmeupT09HhI7ger98zDAwAK\n" + "CRDDEsl9qfdqTwR6AP9Xftw8xZ7/MWhYImk/xheqPy07K4qo3T1pGKUvUqjWQQEA\n" + "hE3r0oTcJn+KVCwGjF6AYiLOzO/R1x5bSlYD3FeJ3Qo=\n" + - "=wsFa\n" + + "=rYoa\n" + "-----END PGP PRIVATE KEY BLOCK-----"; public static final String PRIMARY_KEY_ON_CARD = "-----BEGIN PGP PRIVATE KEY BLOCK-----\n" + @@ -78,7 +79,7 @@ public class GnuDummyKeyUtilTest { "Comment: Hardy Hardware \n" + "\n" + "lEwEY1vSiBYJKwYBBAHaRw8BAQdAQ58lZn/HOtg+1b1KS18odyQ6M4LaDdbJAyRf\n" + - "eBwCeTT/AGUAR05VAhAAAQIDBAUGBwgJCgsMDQ4PtCBIYXJkeSBIYXJkd2FyZSA8\n" + + "eBwCeTT+AGUAR05VAhAAAQIDBAUGBwgJCgsMDQ4PtCBIYXJkeSBIYXJkd2FyZSA8\n" + "aGFyZHlAaGFyZC53YXJlPoiPBBMWCgBBBQJjW9KICRDDEsl9qfdqTxYhBAH9q2zg\n" + "SlB4ef5KGMMSyX2p92pPAp4BApsBBRYCAwEABAsJCAcFFQoJCAsCmQEAAPk2AP92\n" + "2T5TQ7hukFlpxX3ThMhieJnECGY5Eqt5U0/vEY1XdgD/eE1Ml9qqx6QGcaNKe8de\n" + @@ -94,7 +95,7 @@ public class GnuDummyKeyUtilTest { "4+Z3gvtXopmeupT09HhI7ger98zDAwAKCRDDEsl9qfdqTwR6AP9Xftw8xZ7/MWhY\n" + "Imk/xheqPy07K4qo3T1pGKUvUqjWQQEAhE3r0oTcJn+KVCwGjF6AYiLOzO/R1x5b\n" + "SlYD3FeJ3Qo=\n" + - "=s+B1\n" + + "=zQLi\n" + "-----END PGP PRIVATE KEY BLOCK-----"; public static final String ENCRYPTION_KEY_ON_CARD = "-----BEGIN PGP PRIVATE KEY BLOCK-----\n" + @@ -108,7 +109,7 @@ public class GnuDummyKeyUtilTest { "qfdqTxYhBAH9q2zgSlB4ef5KGMMSyX2p92pPAp4BApsBBRYCAwEABAsJCAcFFQoJ\n" + "CAsCmQEAAPk2AP922T5TQ7hukFlpxX3ThMhieJnECGY5Eqt5U0/vEY1XdgD/eE1M\n" + "l9qqx6QGcaNKe8deMe3EhTant6mS9tqMHp2/3gmcUQRjW9KIEgorBgEEAZdVAQUB\n" + - "AQdAVXBLNvNmFh9KX6iLmdNJM28Zc9PGnzEoAD9+T4p0lDwDAQgH/wBlAEdOVQIQ\n" + + "AQdAVXBLNvNmFh9KX6iLmdNJM28Zc9PGnzEoAD9+T4p0lDwDAQgH/gBlAEdOVQIQ\n" + "AAECAwQFBgcICQoLDA0OD4h1BBgWCgAdBQJjW9KIAp4BApsMBRYCAwEABAsJCAcF\n" + "FQoJCAsACgkQwxLJfan3ak/JyQD9GBj0vjtYZAf5Fi0eEKdiAgs0yZrQPkMs6eL+\n" + "83te770A/jG0DeJy+88fOfWTj+mixO98PZPnQ0MybWC/1QUTvP0BnFgEY1vSiBYJ\n" + @@ -119,7 +120,7 @@ public class GnuDummyKeyUtilTest { "4+Z3gvtXopmeupT09HhI7ger98zDAwAKCRDDEsl9qfdqTwR6AP9Xftw8xZ7/MWhY\n" + "Imk/xheqPy07K4qo3T1pGKUvUqjWQQEAhE3r0oTcJn+KVCwGjF6AYiLOzO/R1x5b\n" + "SlYD3FeJ3Qo=\n" + - "=TPAl\n" + + "=7OZu\n" + "-----END PGP PRIVATE KEY BLOCK-----"; public static final String SIGNATURE_KEY_ON_CARD = "-----BEGIN PGP PRIVATE KEY BLOCK-----\n" + @@ -138,13 +139,13 @@ public class GnuDummyKeyUtilTest { "BRYCAwEABAsJCAcFFQoJCAsACgkQwxLJfan3ak/JyQD9GBj0vjtYZAf5Fi0eEKdi\n" + "Ags0yZrQPkMs6eL+83te770A/jG0DeJy+88fOfWTj+mixO98PZPnQ0MybWC/1QUT\n" + "vP0BnEwEY1vSiBYJKwYBBAHaRw8BAQdAvSYTD60t8vx10dSEBACUoIfVCpeOB30D\n" + - "6nfwJtbDT0b/AGUAR05VAhAAAQIDBAUGBwgJCgsMDQ4PiNUEGBYKAH0FAmNb0ogC\n" + + "6nfwJtbDT0b+AGUAR05VAhAAAQIDBAUGBwgJCgsMDQ4PiNUEGBYKAH0FAmNb0ogC\n" + "ngECmwIFFgIDAQAECwkIBwUVCgkIC18gBBkWCgAGBQJjW9KIAAoJEJQCL6VtwFtJ\n" + "DmMBAKqsGfRFQxJXyPgugWBgEaO5lt9fMM0yUxa76cmSWe5fAQD2oLSEW1GOgIs6\n" + "4+Z3gvtXopmeupT09HhI7ger98zDAwAKCRDDEsl9qfdqTwR6AP9Xftw8xZ7/MWhY\n" + "Imk/xheqPy07K4qo3T1pGKUvUqjWQQEAhE3r0oTcJn+KVCwGjF6AYiLOzO/R1x5b\n" + "SlYD3FeJ3Qo=\n" + - "=p8I9\n" + + "=GpEw\n" + "-----END PGP PRIVATE KEY BLOCK-----"; @Test @@ -156,7 +157,7 @@ public class GnuDummyKeyUtilTest { .divertPrivateKeysToCard(GnuDummyKeyUtil.KeyFilter.any(), cardSerial); for (PGPSecretKey key : onCard) { - assertEquals(255, key.getS2KUsage()); + assertEquals(SecretKeyPacket.USAGE_SHA1, key.getS2KUsage()); S2K s2K = key.getS2K(); assertEquals(S2K.GNU_PROTECTION_MODE_DIVERT_TO_CARD, s2K.getProtectionMode()); } From 3b9159e6321dd359230a01b29abd9733e69a91d4 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Sat, 29 Oct 2022 14:09:41 +0200 Subject: [PATCH 72/80] Rename GnuPGDummyExtension + GnuPGDummyKeyUtil --- ...xtension.java => GnuPGDummyExtension.java} | 4 +-- ...mmyKeyUtil.java => GnuPGDummyKeyUtil.java} | 31 ++++++++++++------- .../HardwareSecurityTest.java | 10 +++--- ...ilTest.java => GnuPGDummyKeyUtilTest.java} | 18 +++++------ 4 files changed, 36 insertions(+), 27 deletions(-) rename pgpainless-core/src/main/java/org/pgpainless/key/gnu_dummy_s2k/{GNUExtension.java => GnuPGDummyExtension.java} (88%) rename pgpainless-core/src/main/java/org/pgpainless/key/gnu_dummy_s2k/{GnuDummyKeyUtil.java => GnuPGDummyKeyUtil.java} (81%) rename pgpainless-core/src/test/java/org/pgpainless/key/gnu_dummy_s2k/{GnuDummyKeyUtilTest.java => GnuPGDummyKeyUtilTest.java} (93%) diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/gnu_dummy_s2k/GNUExtension.java b/pgpainless-core/src/main/java/org/pgpainless/key/gnu_dummy_s2k/GnuPGDummyExtension.java similarity index 88% rename from pgpainless-core/src/main/java/org/pgpainless/key/gnu_dummy_s2k/GNUExtension.java rename to pgpainless-core/src/main/java/org/pgpainless/key/gnu_dummy_s2k/GnuPGDummyExtension.java index e829bd7b..9f75bf7e 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/key/gnu_dummy_s2k/GNUExtension.java +++ b/pgpainless-core/src/main/java/org/pgpainless/key/gnu_dummy_s2k/GnuPGDummyExtension.java @@ -6,7 +6,7 @@ package org.pgpainless.key.gnu_dummy_s2k; import org.bouncycastle.bcpg.S2K; -public enum GNUExtension { +public enum GnuPGDummyExtension { /** * Do not store the secret part at all. @@ -21,7 +21,7 @@ public enum GNUExtension { private final int id; - GNUExtension(int id) { + GnuPGDummyExtension(int id) { this.id = id; } diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/gnu_dummy_s2k/GnuDummyKeyUtil.java b/pgpainless-core/src/main/java/org/pgpainless/key/gnu_dummy_s2k/GnuPGDummyKeyUtil.java similarity index 81% rename from pgpainless-core/src/main/java/org/pgpainless/key/gnu_dummy_s2k/GnuDummyKeyUtil.java rename to pgpainless-core/src/main/java/org/pgpainless/key/gnu_dummy_s2k/GnuPGDummyKeyUtil.java index 7817a676..3a913894 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/key/gnu_dummy_s2k/GnuDummyKeyUtil.java +++ b/pgpainless-core/src/main/java/org/pgpainless/key/gnu_dummy_s2k/GnuPGDummyKeyUtil.java @@ -17,11 +17,15 @@ import java.util.Collection; import java.util.List; /** - * This class can be used to remove private keys from secret keys. + * This class can be used to remove private keys from secret software-keys by replacing them with + * stub secret keys in the style of GnuPGs proprietary extensions. + * + * @see + * GnuPGs doc/DETAILS - GNU extensions to the S2K algorithm */ -public final class GnuDummyKeyUtil { +public final class GnuPGDummyKeyUtil { - private GnuDummyKeyUtil() { + private GnuPGDummyKeyUtil() { } @@ -45,18 +49,18 @@ public final class GnuDummyKeyUtil { /** * Remove all private keys that match the given {@link KeyFilter} from the key ring and replace them with - * GNU_DUMMY keys with S2K protection mode {@link GNUExtension#NO_PRIVATE_KEY}. + * GNU_DUMMY keys with S2K protection mode {@link GnuPGDummyExtension#NO_PRIVATE_KEY}. * * @param filter filter to select keys for removal * @return modified key ring */ public PGPSecretKeyRing removePrivateKeys(KeyFilter filter) { - return replacePrivateKeys(GNUExtension.NO_PRIVATE_KEY, null, filter); + return replacePrivateKeys(GnuPGDummyExtension.NO_PRIVATE_KEY, null, filter); } /** * Remove all private keys that match the given {@link KeyFilter} from the key ring and replace them with - * GNU_DUMMY keys with S2K protection mode {@link GNUExtension#DIVERT_TO_CARD}. + * GNU_DUMMY keys with S2K protection mode {@link GnuPGDummyExtension#DIVERT_TO_CARD}. * This method will set the serial number of the card to 0x00000000000000000000000000000000. * * NOTE: This method does not actually move any keys to a card. @@ -70,7 +74,7 @@ public final class GnuDummyKeyUtil { /** * Remove all private keys that match the given {@link KeyFilter} from the key ring and replace them with - * GNU_DUMMY keys with S2K protection mode {@link GNUExtension#DIVERT_TO_CARD}. + * GNU_DUMMY keys with S2K protection mode {@link GnuPGDummyExtension#DIVERT_TO_CARD}. * This method will include the card serial number into the encoded dummy key. * * NOTE: This method does not actually move any keys to a card. @@ -83,10 +87,10 @@ public final class GnuDummyKeyUtil { if (cardSerialNumber != null && cardSerialNumber.length > 16) { throw new IllegalArgumentException("Card serial number length cannot exceed 16 bytes."); } - return replacePrivateKeys(GNUExtension.DIVERT_TO_CARD, cardSerialNumber, filter); + return replacePrivateKeys(GnuPGDummyExtension.DIVERT_TO_CARD, cardSerialNumber, filter); } - private PGPSecretKeyRing replacePrivateKeys(GNUExtension extension, byte[] serial, KeyFilter filter) { + private PGPSecretKeyRing replacePrivateKeys(GnuPGDummyExtension extension, byte[] serial, KeyFilter filter) { byte[] encodedSerial = serial != null ? encodeSerial(serial) : null; S2K s2k = extensionToS2K(extension); @@ -122,12 +126,16 @@ public final class GnuDummyKeyUtil { return encoded; } - private S2K extensionToS2K(@Nonnull GNUExtension extension) { - return S2K.gnuDummyS2K(extension == GNUExtension.DIVERT_TO_CARD ? + private S2K extensionToS2K(@Nonnull GnuPGDummyExtension extension) { + return S2K.gnuDummyS2K(extension == GnuPGDummyExtension.DIVERT_TO_CARD ? S2K.GNUDummyParams.divertToCard() : S2K.GNUDummyParams.noPrivateKey()); } } + /** + * Filter for selecting keys. + */ + @FunctionalInterface public interface KeyFilter { /** @@ -140,6 +148,7 @@ public final class GnuDummyKeyUtil { /** * Select any key. + * * @return filter */ static KeyFilter any() { diff --git a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/HardwareSecurityTest.java b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/HardwareSecurityTest.java index f1606cec..a2160edf 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/HardwareSecurityTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/HardwareSecurityTest.java @@ -17,7 +17,7 @@ import org.bouncycastle.openpgp.PGPSecretKeyRing; import org.junit.jupiter.api.Test; import org.pgpainless.PGPainless; import org.pgpainless.key.SubkeyIdentifier; -import org.pgpainless.key.gnu_dummy_s2k.GnuDummyKeyUtil; +import org.pgpainless.key.gnu_dummy_s2k.GnuPGDummyKeyUtil; import org.pgpainless.key.util.KeyIdUtil; public class HardwareSecurityTest { @@ -53,8 +53,8 @@ public class HardwareSecurityTest { assertTrue(HardwareSecurity.getIdsOfHardwareBackedKeys(secretKeys).isEmpty()); long encryptionKeyId = KeyIdUtil.fromLongKeyId("0AAD8F5891262F50"); - PGPSecretKeyRing withHardwareBackedEncryptionKey = GnuDummyKeyUtil.modify(secretKeys) - .divertPrivateKeysToCard(GnuDummyKeyUtil.KeyFilter.only(encryptionKeyId)); + PGPSecretKeyRing withHardwareBackedEncryptionKey = GnuPGDummyKeyUtil.modify(secretKeys) + .divertPrivateKeysToCard(GnuPGDummyKeyUtil.KeyFilter.only(encryptionKeyId)); Set hardwareBackedKeys = HardwareSecurity .getIdsOfHardwareBackedKeys(withHardwareBackedEncryptionKey); @@ -67,8 +67,8 @@ public class HardwareSecurityTest { PGPSecretKeyRing secretKeys = PGPainless.readKeyRing().secretKeyRing(KEY); assertTrue(HardwareSecurity.getIdsOfHardwareBackedKeys(secretKeys).isEmpty()); - PGPSecretKeyRing withHardwareBackedEncryptionKey = GnuDummyKeyUtil.modify(secretKeys) - .divertPrivateKeysToCard(GnuDummyKeyUtil.KeyFilter.any()); + PGPSecretKeyRing withHardwareBackedEncryptionKey = GnuPGDummyKeyUtil.modify(secretKeys) + .divertPrivateKeysToCard(GnuPGDummyKeyUtil.KeyFilter.any()); Set expected = new HashSet<>(); for (PGPSecretKey key : secretKeys) { expected.add(new SubkeyIdentifier(secretKeys, key.getKeyID())); diff --git a/pgpainless-core/src/test/java/org/pgpainless/key/gnu_dummy_s2k/GnuDummyKeyUtilTest.java b/pgpainless-core/src/test/java/org/pgpainless/key/gnu_dummy_s2k/GnuPGDummyKeyUtilTest.java similarity index 93% rename from pgpainless-core/src/test/java/org/pgpainless/key/gnu_dummy_s2k/GnuDummyKeyUtilTest.java rename to pgpainless-core/src/test/java/org/pgpainless/key/gnu_dummy_s2k/GnuPGDummyKeyUtilTest.java index 99966903..1fc3b892 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/key/gnu_dummy_s2k/GnuDummyKeyUtilTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/key/gnu_dummy_s2k/GnuPGDummyKeyUtilTest.java @@ -17,7 +17,7 @@ import java.io.IOException; import static org.junit.jupiter.api.Assertions.assertArrayEquals; import static org.junit.jupiter.api.Assertions.assertEquals; -public class GnuDummyKeyUtilTest { +public class GnuPGDummyKeyUtilTest { // normal, non-hw-backed key private static final String FULL_KEY = "-----BEGIN PGP PRIVATE KEY BLOCK-----\n" + "Version: PGPainless\n" + @@ -153,8 +153,8 @@ public class GnuDummyKeyUtilTest { PGPSecretKeyRing secretKeys = PGPainless.readKeyRing().secretKeyRing(FULL_KEY); PGPSecretKeyRing expected = PGPainless.readKeyRing().secretKeyRing(ALL_KEYS_ON_CARD); - PGPSecretKeyRing onCard = GnuDummyKeyUtil.modify(secretKeys) - .divertPrivateKeysToCard(GnuDummyKeyUtil.KeyFilter.any(), cardSerial); + PGPSecretKeyRing onCard = GnuPGDummyKeyUtil.modify(secretKeys) + .divertPrivateKeysToCard(GnuPGDummyKeyUtil.KeyFilter.any(), cardSerial); for (PGPSecretKey key : onCard) { assertEquals(SecretKeyPacket.USAGE_SHA1, key.getS2KUsage()); @@ -170,8 +170,8 @@ public class GnuDummyKeyUtilTest { PGPSecretKeyRing secretKeys = PGPainless.readKeyRing().secretKeyRing(FULL_KEY); PGPSecretKeyRing expected = PGPainless.readKeyRing().secretKeyRing(PRIMARY_KEY_ON_CARD); - PGPSecretKeyRing onCard = GnuDummyKeyUtil.modify(secretKeys) - .divertPrivateKeysToCard(GnuDummyKeyUtil.KeyFilter.only(primaryKeyId), cardSerial); + PGPSecretKeyRing onCard = GnuPGDummyKeyUtil.modify(secretKeys) + .divertPrivateKeysToCard(GnuPGDummyKeyUtil.KeyFilter.only(primaryKeyId), cardSerial); assertArrayEquals(expected.getEncoded(), onCard.getEncoded()); } @@ -181,8 +181,8 @@ public class GnuDummyKeyUtilTest { PGPSecretKeyRing secretKeys = PGPainless.readKeyRing().secretKeyRing(FULL_KEY); PGPSecretKeyRing expected = PGPainless.readKeyRing().secretKeyRing(ENCRYPTION_KEY_ON_CARD); - PGPSecretKeyRing onCard = GnuDummyKeyUtil.modify(secretKeys) - .divertPrivateKeysToCard(GnuDummyKeyUtil.KeyFilter.only(encryptionKeyId), cardSerial); + PGPSecretKeyRing onCard = GnuPGDummyKeyUtil.modify(secretKeys) + .divertPrivateKeysToCard(GnuPGDummyKeyUtil.KeyFilter.only(encryptionKeyId), cardSerial); assertArrayEquals(expected.getEncoded(), onCard.getEncoded()); } @@ -192,8 +192,8 @@ public class GnuDummyKeyUtilTest { PGPSecretKeyRing secretKeys = PGPainless.readKeyRing().secretKeyRing(FULL_KEY); PGPSecretKeyRing expected = PGPainless.readKeyRing().secretKeyRing(SIGNATURE_KEY_ON_CARD); - PGPSecretKeyRing onCard = GnuDummyKeyUtil.modify(secretKeys) - .divertPrivateKeysToCard(GnuDummyKeyUtil.KeyFilter.only(signatureKeyId), cardSerial); + PGPSecretKeyRing onCard = GnuPGDummyKeyUtil.modify(secretKeys) + .divertPrivateKeysToCard(GnuPGDummyKeyUtil.KeyFilter.only(signatureKeyId), cardSerial); assertArrayEquals(expected.getEncoded(), onCard.getEncoded()); } From b74e7bcb7c38c1037944a677e33a8d1150aebc1a Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Sat, 29 Oct 2022 14:51:39 +0200 Subject: [PATCH 73/80] Add test for decryption with removed private key --- .../HardwareSecurity.java | 31 ------- .../key/gnu_dummy_s2k/GnuPGDummyKeyUtil.java | 30 +++++++ .../HardwareSecurityTest.java | 82 ------------------- ...DecryptWithUnavailableGnuDummyKeyTest.java | 52 ++++++++++++ .../gnu_dummy_s2k/GnuPGDummyKeyUtilTest.java | 77 +++++++++++++++++ 5 files changed, 159 insertions(+), 113 deletions(-) delete mode 100644 pgpainless-core/src/test/java/org/pgpainless/decryption_verification/HardwareSecurityTest.java create mode 100644 pgpainless-core/src/test/java/org/pgpainless/decryption_verification/TryDecryptWithUnavailableGnuDummyKeyTest.java diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/HardwareSecurity.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/HardwareSecurity.java index 6d9719dd..daf902d4 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/HardwareSecurity.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/HardwareSecurity.java @@ -4,18 +4,12 @@ package org.pgpainless.decryption_verification; -import org.bouncycastle.bcpg.S2K; import org.bouncycastle.openpgp.PGPException; -import org.bouncycastle.openpgp.PGPSecretKey; -import org.bouncycastle.openpgp.PGPSecretKeyRing; import org.bouncycastle.openpgp.operator.PGPDataDecryptor; import org.bouncycastle.openpgp.operator.PublicKeyDataDecryptorFactory; import org.bouncycastle.openpgp.operator.bc.BcPublicKeyDataDecryptorFactory; import org.pgpainless.key.SubkeyIdentifier; -import java.util.HashSet; -import java.util.Set; - /** * Enable integration of hardware-backed OpenPGP keys. */ @@ -41,31 +35,6 @@ public class HardwareSecurity { } - /** - * Return the key-ids of all keys which appear to be stored on a hardware token / smartcard. - * - * @param secretKeys secret keys - * @return set of keys with S2K type DIVERT_TO_CARD or GNU_DUMMY_S2K - */ - public static Set getIdsOfHardwareBackedKeys(PGPSecretKeyRing secretKeys) { - Set hardwareBackedKeys = new HashSet<>(); - for (PGPSecretKey secretKey : secretKeys) { - S2K s2K = secretKey.getS2K(); - if (s2K == null) { - continue; - } - - int type = s2K.getType(); - int mode = s2K.getProtectionMode(); - // TODO: Is GNU_DUMMY_S2K appropriate? - if (type == S2K.GNU_DUMMY_S2K && mode == S2K.GNU_PROTECTION_MODE_DIVERT_TO_CARD) { - SubkeyIdentifier hardwareBackedKey = new SubkeyIdentifier(secretKeys, secretKey.getKeyID()); - hardwareBackedKeys.add(hardwareBackedKey); - } - } - return hardwareBackedKeys; - } - /** * Implementation of {@link PublicKeyDataDecryptorFactory} which delegates decryption of encrypted session keys * to a {@link DecryptionCallback}. diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/gnu_dummy_s2k/GnuPGDummyKeyUtil.java b/pgpainless-core/src/main/java/org/pgpainless/key/gnu_dummy_s2k/GnuPGDummyKeyUtil.java index 3a913894..64c7ed26 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/key/gnu_dummy_s2k/GnuPGDummyKeyUtil.java +++ b/pgpainless-core/src/main/java/org/pgpainless/key/gnu_dummy_s2k/GnuPGDummyKeyUtil.java @@ -10,11 +10,14 @@ import org.bouncycastle.bcpg.SecretKeyPacket; import org.bouncycastle.bcpg.SecretSubkeyPacket; import org.bouncycastle.openpgp.PGPSecretKey; import org.bouncycastle.openpgp.PGPSecretKeyRing; +import org.pgpainless.key.SubkeyIdentifier; import javax.annotation.Nonnull; import java.util.ArrayList; import java.util.Collection; +import java.util.HashSet; import java.util.List; +import java.util.Set; /** * This class can be used to remove private keys from secret software-keys by replacing them with @@ -29,6 +32,33 @@ public final class GnuPGDummyKeyUtil { } + /** + * Return the key-ids of all keys which appear to be stored on a hardware token / smartcard by GnuPG. + * Note, that this functionality is based on GnuPGs proprietary S2K extensions, which are not strictly required + * for dealing with hardware-backed keys. + * + * @param secretKeys secret keys + * @return set of keys with S2K type GNU_DUMMY_S2K and protection mode DIVERT_TO_CARD + */ + public static Set getIdsOfKeysWithGnuPGS2KDivertedToCard(PGPSecretKeyRing secretKeys) { + Set hardwareBackedKeys = new HashSet<>(); + for (PGPSecretKey secretKey : secretKeys) { + S2K s2K = secretKey.getS2K(); + if (s2K == null) { + continue; + } + + int type = s2K.getType(); + int mode = s2K.getProtectionMode(); + // TODO: Is GNU_DUMMY_S2K appropriate? + if (type == S2K.GNU_DUMMY_S2K && mode == S2K.GNU_PROTECTION_MODE_DIVERT_TO_CARD) { + SubkeyIdentifier hardwareBackedKey = new SubkeyIdentifier(secretKeys, secretKey.getKeyID()); + hardwareBackedKeys.add(hardwareBackedKey); + } + } + return hardwareBackedKeys; + } + /** * Modify the given {@link PGPSecretKeyRing}. * diff --git a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/HardwareSecurityTest.java b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/HardwareSecurityTest.java deleted file mode 100644 index a2160edf..00000000 --- a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/HardwareSecurityTest.java +++ /dev/null @@ -1,82 +0,0 @@ -// SPDX-FileCopyrightText: 2022 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.decryption_verification; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertTrue; - -import java.io.IOException; -import java.util.Collections; -import java.util.HashSet; -import java.util.Set; - -import org.bouncycastle.openpgp.PGPSecretKey; -import org.bouncycastle.openpgp.PGPSecretKeyRing; -import org.junit.jupiter.api.Test; -import org.pgpainless.PGPainless; -import org.pgpainless.key.SubkeyIdentifier; -import org.pgpainless.key.gnu_dummy_s2k.GnuPGDummyKeyUtil; -import org.pgpainless.key.util.KeyIdUtil; - -public class HardwareSecurityTest { - - private static final String KEY = "-----BEGIN PGP PRIVATE KEY BLOCK-----\n" + - "Version: PGPainless\n" + - "Comment: DE2E 9AB2 6650 8191 53E7 D599 C176 507F 2B5D 43B3\n" + - "Comment: Alice \n" + - "\n" + - "lFgEY1vjgRYJKwYBBAHaRw8BAQdAXjLoPTOIOdvlFT2Nt3rcvLTVx5ujPBGghZ5S\n" + - "D5tEnyoAAP0fAUJTiPrxZYdzs6MP0KFo+Nmr/wb1PJHTkzmYpt4wkRKBtBxBbGlj\n" + - "ZSA8YWxpY2VAcGdwYWlubGVzcy5vcmc+iI8EExYKAEEFAmNb44EJEMF2UH8rXUOz\n" + - "FiEE3i6asmZQgZFT59WZwXZQfytdQ7MCngECmwEFFgIDAQAECwkIBwUVCgkICwKZ\n" + - "AQAAHLYA/AgW+YrpU+UqrwX2dhY6RAfgHTTMU89RHjaTHJx8pLrBAP4gthGof00a\n" + - "XEjwTWteDOO049SIp2AUfj9deJqtrQcHD5xdBGNb44ESCisGAQQBl1UBBQEBB0DN\n" + - "vUT3awa3YLmwf41LRpPrm7B87AOHfYIP8S9QJ4GDJgMBCAcAAP9bwlSaF+lti8JY\n" + - "qKFO3qt3ZYQMu1l/LRBle89ZB4zD+BDOiHUEGBYKAB0FAmNb44ECngECmwwFFgID\n" + - "AQAECwkIBwUVCgkICwAKCRDBdlB/K11Ds/TsAP9kvpUrCWnrWGq+a9n1CqEfCMX5\n" + - "cT+qzrwNf+J0L22KowD+M9SVO0qssiAqutLE9h9dGYLbEiFvsHzK3WSnjKYbIgac\n" + - "WARjW+OBFgkrBgEEAdpHDwEBB0BCPh8M5TnXSmG6Ygwp4j5RR4u3hmxl8CYjX4h/\n" + - "XtvvNwAA/RP04coSrLHVI6vUfbJk4MhWYeyhJBRYY0vGp7yq+wVtEpKI1QQYFgoA\n" + - "fQUCY1vjgQKeAQKbAgUWAgMBAAQLCQgHBRUKCQgLXyAEGRYKAAYFAmNb44EACgkQ\n" + - "mlozJSF7rXQW+AD/TA3YBxTd+YbBSwfgqzWNbfT9BBcFrdn3uPCsbvfmqXoA/3oj\n" + - "oupkgoaXesrGxn2k9hW9/GBXSvNcgY2txZ6/oYoIAAoJEMF2UH8rXUOziZ4A/0Xl\n" + - "xSZJWmkRpBh5AO8Cnqosz6j947IYAxS16ay+sIOHAP9aN9CUNJIIdHnHdFHO4GZz\n" + - "ejjknn4wt8NVJP97JxlnBQ==\n" + - "=qSQb\n" + - "-----END PGP PRIVATE KEY BLOCK-----"; - - @Test - public void testGetSingleIdOfHardwareBackedKey() throws IOException { - PGPSecretKeyRing secretKeys = PGPainless.readKeyRing().secretKeyRing(KEY); - assertTrue(HardwareSecurity.getIdsOfHardwareBackedKeys(secretKeys).isEmpty()); - long encryptionKeyId = KeyIdUtil.fromLongKeyId("0AAD8F5891262F50"); - - PGPSecretKeyRing withHardwareBackedEncryptionKey = GnuPGDummyKeyUtil.modify(secretKeys) - .divertPrivateKeysToCard(GnuPGDummyKeyUtil.KeyFilter.only(encryptionKeyId)); - - Set hardwareBackedKeys = HardwareSecurity - .getIdsOfHardwareBackedKeys(withHardwareBackedEncryptionKey); - assertEquals(Collections.singleton(new SubkeyIdentifier(secretKeys, encryptionKeyId)), hardwareBackedKeys); - } - - - @Test - public void testGetIdsOfFullyHardwareBackedKey() throws IOException { - PGPSecretKeyRing secretKeys = PGPainless.readKeyRing().secretKeyRing(KEY); - assertTrue(HardwareSecurity.getIdsOfHardwareBackedKeys(secretKeys).isEmpty()); - - PGPSecretKeyRing withHardwareBackedEncryptionKey = GnuPGDummyKeyUtil.modify(secretKeys) - .divertPrivateKeysToCard(GnuPGDummyKeyUtil.KeyFilter.any()); - Set expected = new HashSet<>(); - for (PGPSecretKey key : secretKeys) { - expected.add(new SubkeyIdentifier(secretKeys, key.getKeyID())); - } - - Set hardwareBackedKeys = HardwareSecurity - .getIdsOfHardwareBackedKeys(withHardwareBackedEncryptionKey); - - assertEquals(expected, hardwareBackedKeys); - } -} diff --git a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/TryDecryptWithUnavailableGnuDummyKeyTest.java b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/TryDecryptWithUnavailableGnuDummyKeyTest.java new file mode 100644 index 00000000..79fa7a8e --- /dev/null +++ b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/TryDecryptWithUnavailableGnuDummyKeyTest.java @@ -0,0 +1,52 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.decryption_verification; + +import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.PGPPublicKeyRing; +import org.bouncycastle.openpgp.PGPSecretKeyRing; +import org.bouncycastle.util.io.Streams; +import org.junit.jupiter.api.Test; +import org.pgpainless.PGPainless; +import org.pgpainless.encryption_signing.EncryptionOptions; +import org.pgpainless.encryption_signing.EncryptionStream; +import org.pgpainless.encryption_signing.ProducerOptions; +import org.pgpainless.key.gnu_dummy_s2k.GnuPGDummyKeyUtil; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.security.InvalidAlgorithmParameterException; +import java.security.NoSuchAlgorithmException; + +import static org.junit.jupiter.api.Assertions.assertThrows; + +public class TryDecryptWithUnavailableGnuDummyKeyTest { + + @Test + public void testAttemptToDecryptWithRemovedPrivateKeysThrows() + throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, IOException { + PGPSecretKeyRing secretKeys = PGPainless.generateKeyRing() + .modernKeyRing("Hardy Hardware "); + PGPPublicKeyRing certificate = PGPainless.extractCertificate(secretKeys); + + ByteArrayOutputStream ciphertextOut = new ByteArrayOutputStream(); + EncryptionStream encryptionStream = PGPainless.encryptAndOrSign() + .onOutputStream(ciphertextOut) + .withOptions( + ProducerOptions.encrypt(EncryptionOptions.get().addRecipient(certificate))); + ByteArrayInputStream plaintextIn = new ByteArrayInputStream("Hello, World!\n".getBytes()); + Streams.pipeAll(plaintextIn, encryptionStream); + encryptionStream.close(); + + PGPSecretKeyRing removedKeys = GnuPGDummyKeyUtil.modify(secretKeys) + .removePrivateKeys(GnuPGDummyKeyUtil.KeyFilter.any()); + + ByteArrayInputStream ciphertextIn = new ByteArrayInputStream(ciphertextOut.toByteArray()); + assertThrows(PGPException.class, () -> PGPainless.decryptAndOrVerify() + .onInputStream(ciphertextIn) + .withOptions(ConsumerOptions.get().addDecryptionKey(removedKeys))); + } +} diff --git a/pgpainless-core/src/test/java/org/pgpainless/key/gnu_dummy_s2k/GnuPGDummyKeyUtilTest.java b/pgpainless-core/src/test/java/org/pgpainless/key/gnu_dummy_s2k/GnuPGDummyKeyUtilTest.java index 1fc3b892..e175e27f 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/key/gnu_dummy_s2k/GnuPGDummyKeyUtilTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/key/gnu_dummy_s2k/GnuPGDummyKeyUtilTest.java @@ -10,12 +10,17 @@ import org.bouncycastle.openpgp.PGPSecretKey; import org.bouncycastle.openpgp.PGPSecretKeyRing; import org.junit.jupiter.api.Test; import org.pgpainless.PGPainless; +import org.pgpainless.key.SubkeyIdentifier; import org.pgpainless.key.util.KeyIdUtil; import java.io.IOException; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; import static org.junit.jupiter.api.Assertions.assertArrayEquals; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; public class GnuPGDummyKeyUtilTest { // normal, non-hw-backed key @@ -73,6 +78,29 @@ public class GnuPGDummyKeyUtilTest { "=rYoa\n" + "-----END PGP PRIVATE KEY BLOCK-----"; + public static final String ALL_KEYS_REMOVED = "-----BEGIN PGP PRIVATE KEY BLOCK-----\n" + + "Version: PGPainless\n" + + "Comment: 01FD AB6C E04A 5078 79FE 4A18 C312 C97D A9F7 6A4F\n" + + "Comment: Hardy Hardware \n" + + "\n" + + "lDsEY1vSiBYJKwYBBAHaRw8BAQdAQ58lZn/HOtg+1b1KS18odyQ6M4LaDdbJAyRf\n" + + "eBwCeTT+AGUAR05VAbQgSGFyZHkgSGFyZHdhcmUgPGhhcmR5QGhhcmQud2FyZT6I\n" + + "jwQTFgoAQQUCY1vSiAkQwxLJfan3ak8WIQQB/ats4EpQeHn+ShjDEsl9qfdqTwKe\n" + + "AQKbAQUWAgMBAAQLCQgHBRUKCQgLApkBAAD5NgD/dtk+U0O4bpBZacV904TIYniZ\n" + + "xAhmORKreVNP7xGNV3YA/3hNTJfaqsekBnGjSnvHXjHtxIU2p7epkvbajB6dv94J\n" + + "nEAEY1vSiBIKKwYBBAGXVQEFAQEHQFVwSzbzZhYfSl+oi5nTSTNvGXPTxp8xKAA/\n" + + "fk+KdJQ8AwEIB/4AZQBHTlUBiHUEGBYKAB0FAmNb0ogCngECmwwFFgIDAQAECwkI\n" + + "BwUVCgkICwAKCRDDEsl9qfdqT8nJAP0YGPS+O1hkB/kWLR4Qp2ICCzTJmtA+Qyzp\n" + + "4v7ze17vvQD+MbQN4nL7zx859ZOP6aLE73w9k+dDQzJtYL/VBRO8/QGcOwRjW9KI\n" + + "FgkrBgEEAdpHDwEBB0C9JhMPrS3y/HXR1IQEAJSgh9UKl44HfQPqd/Am1sNPRv4A\n" + + "ZQBHTlUBiNUEGBYKAH0FAmNb0ogCngECmwIFFgIDAQAECwkIBwUVCgkIC18gBBkW\n" + + "CgAGBQJjW9KIAAoJEJQCL6VtwFtJDmMBAKqsGfRFQxJXyPgugWBgEaO5lt9fMM0y\n" + + "Uxa76cmSWe5fAQD2oLSEW1GOgIs64+Z3gvtXopmeupT09HhI7ger98zDAwAKCRDD\n" + + "Esl9qfdqTwR6AP9Xftw8xZ7/MWhYImk/xheqPy07K4qo3T1pGKUvUqjWQQEAhE3r\n" + + "0oTcJn+KVCwGjF6AYiLOzO/R1x5bSlYD3FeJ3Qo=\n" + + "=GEN/\n" + + "-----END PGP PRIVATE KEY BLOCK-----"; + public static final String PRIMARY_KEY_ON_CARD = "-----BEGIN PGP PRIVATE KEY BLOCK-----\n" + "Version: PGPainless\n" + "Comment: 01FD AB6C E04A 5078 79FE 4A18 C312 C97D A9F7 6A4F\n" + @@ -197,4 +225,53 @@ public class GnuPGDummyKeyUtilTest { assertArrayEquals(expected.getEncoded(), onCard.getEncoded()); } + + @Test + public void testRemoveAllKeys() throws IOException { + PGPSecretKeyRing secretKeys = PGPainless.readKeyRing().secretKeyRing(FULL_KEY); + PGPSecretKeyRing expected = PGPainless.readKeyRing().secretKeyRing(ALL_KEYS_REMOVED); + + PGPSecretKeyRing removedSecretKeys = GnuPGDummyKeyUtil.modify(secretKeys) + .removePrivateKeys(GnuPGDummyKeyUtil.KeyFilter.any()); + + for (PGPSecretKey key : removedSecretKeys) { + assertEquals(key.getS2KUsage(), SecretKeyPacket.USAGE_SHA1); + S2K s2k = key.getS2K(); + assertEquals(GnuPGDummyExtension.NO_PRIVATE_KEY.getId(), s2k.getProtectionMode()); + } + + assertArrayEquals(expected.getEncoded(), removedSecretKeys.getEncoded()); + } + + @Test + public void testGetSingleIdOfHardwareBackedKey() throws IOException { + PGPSecretKeyRing secretKeys = PGPainless.readKeyRing().secretKeyRing(FULL_KEY); + assertTrue(GnuPGDummyKeyUtil.getIdsOfKeysWithGnuPGS2KDivertedToCard(secretKeys).isEmpty()); + + PGPSecretKeyRing withHardwareBackedEncryptionKey = GnuPGDummyKeyUtil.modify(secretKeys) + .divertPrivateKeysToCard(GnuPGDummyKeyUtil.KeyFilter.only(encryptionKeyId)); + + Set hardwareBackedKeys = GnuPGDummyKeyUtil + .getIdsOfKeysWithGnuPGS2KDivertedToCard(withHardwareBackedEncryptionKey); + assertEquals(Collections.singleton(new SubkeyIdentifier(secretKeys, encryptionKeyId)), hardwareBackedKeys); + } + + + @Test + public void testGetIdsOfFullyHardwareBackedKey() throws IOException { + PGPSecretKeyRing secretKeys = PGPainless.readKeyRing().secretKeyRing(FULL_KEY); + assertTrue(GnuPGDummyKeyUtil.getIdsOfKeysWithGnuPGS2KDivertedToCard(secretKeys).isEmpty()); + + PGPSecretKeyRing withHardwareBackedEncryptionKey = GnuPGDummyKeyUtil.modify(secretKeys) + .divertPrivateKeysToCard(GnuPGDummyKeyUtil.KeyFilter.any()); + Set expected = new HashSet<>(); + for (PGPSecretKey key : secretKeys) { + expected.add(new SubkeyIdentifier(secretKeys, key.getKeyID())); + } + + Set hardwareBackedKeys = GnuPGDummyKeyUtil + .getIdsOfKeysWithGnuPGS2KDivertedToCard(withHardwareBackedEncryptionKey); + + assertEquals(expected, hardwareBackedKeys); + } } From 25fd3fa1d66f29471e7ca1f5725eefb4435e8248 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Sat, 29 Oct 2022 14:58:18 +0200 Subject: [PATCH 74/80] Move classes related to GNU dummy keys to gnupg package --- .../key/gnu_dummy_s2k => gnupg}/GnuPGDummyExtension.java | 2 +- .../key/gnu_dummy_s2k => gnupg}/GnuPGDummyKeyUtil.java | 2 +- .../{pgpainless/key/gnu_dummy_s2k => gnupg}/package-info.java | 2 +- .../key/gnu_dummy_s2k => gnupg}/GnuPGDummyKeyUtilTest.java | 4 +++- .../TryDecryptWithUnavailableGnuDummyKeyTest.java | 2 +- 5 files changed, 7 insertions(+), 5 deletions(-) rename pgpainless-core/src/main/java/org/{pgpainless/key/gnu_dummy_s2k => gnupg}/GnuPGDummyExtension.java (93%) rename pgpainless-core/src/main/java/org/{pgpainless/key/gnu_dummy_s2k => gnupg}/GnuPGDummyKeyUtil.java (99%) rename pgpainless-core/src/main/java/org/{pgpainless/key/gnu_dummy_s2k => gnupg}/package-info.java (81%) rename pgpainless-core/src/test/java/org/{pgpainless/key/gnu_dummy_s2k => gnupg}/GnuPGDummyKeyUtilTest.java (99%) diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/gnu_dummy_s2k/GnuPGDummyExtension.java b/pgpainless-core/src/main/java/org/gnupg/GnuPGDummyExtension.java similarity index 93% rename from pgpainless-core/src/main/java/org/pgpainless/key/gnu_dummy_s2k/GnuPGDummyExtension.java rename to pgpainless-core/src/main/java/org/gnupg/GnuPGDummyExtension.java index 9f75bf7e..d744e222 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/key/gnu_dummy_s2k/GnuPGDummyExtension.java +++ b/pgpainless-core/src/main/java/org/gnupg/GnuPGDummyExtension.java @@ -2,7 +2,7 @@ // // SPDX-License-Identifier: Apache-2.0 -package org.pgpainless.key.gnu_dummy_s2k; +package org.gnupg; import org.bouncycastle.bcpg.S2K; diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/gnu_dummy_s2k/GnuPGDummyKeyUtil.java b/pgpainless-core/src/main/java/org/gnupg/GnuPGDummyKeyUtil.java similarity index 99% rename from pgpainless-core/src/main/java/org/pgpainless/key/gnu_dummy_s2k/GnuPGDummyKeyUtil.java rename to pgpainless-core/src/main/java/org/gnupg/GnuPGDummyKeyUtil.java index 64c7ed26..983d8e68 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/key/gnu_dummy_s2k/GnuPGDummyKeyUtil.java +++ b/pgpainless-core/src/main/java/org/gnupg/GnuPGDummyKeyUtil.java @@ -2,7 +2,7 @@ // // SPDX-License-Identifier: Apache-2.0 -package org.pgpainless.key.gnu_dummy_s2k; +package org.gnupg; import org.bouncycastle.bcpg.PublicKeyPacket; import org.bouncycastle.bcpg.S2K; diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/gnu_dummy_s2k/package-info.java b/pgpainless-core/src/main/java/org/gnupg/package-info.java similarity index 81% rename from pgpainless-core/src/main/java/org/pgpainless/key/gnu_dummy_s2k/package-info.java rename to pgpainless-core/src/main/java/org/gnupg/package-info.java index 5c2a727e..03268619 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/key/gnu_dummy_s2k/package-info.java +++ b/pgpainless-core/src/main/java/org/gnupg/package-info.java @@ -5,4 +5,4 @@ /** * Utility classes related to creating keys with GNU DUMMY S2K values. */ -package org.pgpainless.key.gnu_dummy_s2k; +package org.gnupg; diff --git a/pgpainless-core/src/test/java/org/pgpainless/key/gnu_dummy_s2k/GnuPGDummyKeyUtilTest.java b/pgpainless-core/src/test/java/org/gnupg/GnuPGDummyKeyUtilTest.java similarity index 99% rename from pgpainless-core/src/test/java/org/pgpainless/key/gnu_dummy_s2k/GnuPGDummyKeyUtilTest.java rename to pgpainless-core/src/test/java/org/gnupg/GnuPGDummyKeyUtilTest.java index e175e27f..b9562f60 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/key/gnu_dummy_s2k/GnuPGDummyKeyUtilTest.java +++ b/pgpainless-core/src/test/java/org/gnupg/GnuPGDummyKeyUtilTest.java @@ -2,12 +2,14 @@ // // SPDX-License-Identifier: Apache-2.0 -package org.pgpainless.key.gnu_dummy_s2k; +package org.gnupg; import org.bouncycastle.bcpg.S2K; import org.bouncycastle.bcpg.SecretKeyPacket; import org.bouncycastle.openpgp.PGPSecretKey; import org.bouncycastle.openpgp.PGPSecretKeyRing; +import org.gnupg.GnuPGDummyExtension; +import org.gnupg.GnuPGDummyKeyUtil; import org.junit.jupiter.api.Test; import org.pgpainless.PGPainless; import org.pgpainless.key.SubkeyIdentifier; diff --git a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/TryDecryptWithUnavailableGnuDummyKeyTest.java b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/TryDecryptWithUnavailableGnuDummyKeyTest.java index 79fa7a8e..419c529d 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/TryDecryptWithUnavailableGnuDummyKeyTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/TryDecryptWithUnavailableGnuDummyKeyTest.java @@ -13,7 +13,7 @@ import org.pgpainless.PGPainless; import org.pgpainless.encryption_signing.EncryptionOptions; import org.pgpainless.encryption_signing.EncryptionStream; import org.pgpainless.encryption_signing.ProducerOptions; -import org.pgpainless.key.gnu_dummy_s2k.GnuPGDummyKeyUtil; +import org.gnupg.GnuPGDummyKeyUtil; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; From 3e120fbf7f866b70bd0e839ccbe227c72e792718 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Sat, 29 Oct 2022 15:12:12 +0200 Subject: [PATCH 75/80] Properly handle failed decryption caused by removed private keys --- .../OpenPgpMessageInputStream.java | 25 +++++++++++++++++++ .../java/org/gnupg/GnuPGDummyKeyUtilTest.java | 24 ++++++++---------- ...DecryptWithUnavailableGnuDummyKeyTest.java | 3 ++- 3 files changed, 38 insertions(+), 14 deletions(-) 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 1b11744c..a509cde2 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 @@ -18,6 +18,7 @@ import javax.annotation.Nonnull; import org.bouncycastle.bcpg.ArmoredInputStream; import org.bouncycastle.bcpg.BCPGInputStream; +import org.bouncycastle.bcpg.S2K; import org.bouncycastle.bcpg.UnsupportedPacketVersionException; import org.bouncycastle.openpgp.PGPCompressedData; import org.bouncycastle.openpgp.PGPEncryptedData; @@ -507,6 +508,14 @@ public class OpenPgpMessageInputStream extends DecryptionStream { } PGPSecretKey secretKey = decryptionKeys.getSecretKey(keyId); SubkeyIdentifier decryptionKeyId = new SubkeyIdentifier(decryptionKeys, secretKey.getKeyID()); + S2K s2K = secretKey.getS2K(); + if (s2K != null) { + int s2kType = s2K.getType(); + if (s2kType >= 100 && s2kType <= 110) { + LOGGER.debug("Skipping PKESK because key " + decryptionKeyId + " has unsupported private S2K specifier " + s2kType); + continue; + } + } LOGGER.debug("Attempt decryption using secret key " + decryptionKeyId); SecretKeyRingProtector protector = options.getSecretKeyProtector(decryptionKeys); @@ -532,6 +541,14 @@ public class OpenPgpMessageInputStream extends DecryptionStream { PGPSecretKeyRing decryptionKeys = decryptionKeyCandidate.getA(); PGPSecretKey secretKey = decryptionKeyCandidate.getB(); SubkeyIdentifier decryptionKeyId = new SubkeyIdentifier(decryptionKeys, secretKey.getKeyID()); + S2K s2K = secretKey.getS2K(); + if (s2K != null) { + int s2kType = s2K.getType(); + if (s2kType >= 100 && s2kType <= 110) { + LOGGER.debug("Skipping PKESK because key " + decryptionKeyId + " has unsupported private S2K specifier " + s2kType); + continue; + } + } LOGGER.debug("Attempt decryption of anonymous PKESK with key " + decryptionKeyId); SecretKeyRingProtector protector = options.getSecretKeyProtector(decryptionKeyCandidate.getA()); if (!protector.hasPassphraseFor(secretKey.getKeyID())) { @@ -567,6 +584,14 @@ public class OpenPgpMessageInputStream extends DecryptionStream { long keyId = secretKey.getKeyID(); PGPSecretKeyRing decryptionKey = getDecryptionKey(keyId); SubkeyIdentifier decryptionKeyId = new SubkeyIdentifier(decryptionKey, keyId); + S2K s2K = secretKey.getS2K(); + if (s2K != null) { + int s2kType = s2K.getType(); + if (s2kType >= 100 && s2kType <= 110) { + LOGGER.debug("Skipping PKESK because key " + decryptionKeyId + " has unsupported private S2K specifier " + s2kType); + continue; + } + } LOGGER.debug("Attempt decryption with key " + decryptionKeyId + " while interactively requesting its passphrase"); SecretKeyRingProtector protector = options.getSecretKeyProtector(decryptionKey); PGPPrivateKey privateKey = UnlockSecretKey.unlockSecretKey(secretKey, protector.getDecryptor(keyId)); diff --git a/pgpainless-core/src/test/java/org/gnupg/GnuPGDummyKeyUtilTest.java b/pgpainless-core/src/test/java/org/gnupg/GnuPGDummyKeyUtilTest.java index b9562f60..87c5b02e 100644 --- a/pgpainless-core/src/test/java/org/gnupg/GnuPGDummyKeyUtilTest.java +++ b/pgpainless-core/src/test/java/org/gnupg/GnuPGDummyKeyUtilTest.java @@ -4,25 +4,23 @@ package org.gnupg; -import org.bouncycastle.bcpg.S2K; -import org.bouncycastle.bcpg.SecretKeyPacket; -import org.bouncycastle.openpgp.PGPSecretKey; -import org.bouncycastle.openpgp.PGPSecretKeyRing; -import org.gnupg.GnuPGDummyExtension; -import org.gnupg.GnuPGDummyKeyUtil; -import org.junit.jupiter.api.Test; -import org.pgpainless.PGPainless; -import org.pgpainless.key.SubkeyIdentifier; -import org.pgpainless.key.util.KeyIdUtil; +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; import java.io.IOException; import java.util.Collections; import java.util.HashSet; import java.util.Set; -import static org.junit.jupiter.api.Assertions.assertArrayEquals; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertTrue; +import org.bouncycastle.bcpg.S2K; +import org.bouncycastle.bcpg.SecretKeyPacket; +import org.bouncycastle.openpgp.PGPSecretKey; +import org.bouncycastle.openpgp.PGPSecretKeyRing; +import org.junit.jupiter.api.Test; +import org.pgpainless.PGPainless; +import org.pgpainless.key.SubkeyIdentifier; +import org.pgpainless.key.util.KeyIdUtil; public class GnuPGDummyKeyUtilTest { // normal, non-hw-backed key diff --git a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/TryDecryptWithUnavailableGnuDummyKeyTest.java b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/TryDecryptWithUnavailableGnuDummyKeyTest.java index 419c529d..640025b1 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/TryDecryptWithUnavailableGnuDummyKeyTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/TryDecryptWithUnavailableGnuDummyKeyTest.java @@ -14,6 +14,7 @@ import org.pgpainless.encryption_signing.EncryptionOptions; import org.pgpainless.encryption_signing.EncryptionStream; import org.pgpainless.encryption_signing.ProducerOptions; import org.gnupg.GnuPGDummyKeyUtil; +import org.pgpainless.exception.MissingDecryptionMethodException; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; @@ -45,7 +46,7 @@ public class TryDecryptWithUnavailableGnuDummyKeyTest { .removePrivateKeys(GnuPGDummyKeyUtil.KeyFilter.any()); ByteArrayInputStream ciphertextIn = new ByteArrayInputStream(ciphertextOut.toByteArray()); - assertThrows(PGPException.class, () -> PGPainless.decryptAndOrVerify() + assertThrows(MissingDecryptionMethodException.class, () -> PGPainless.decryptAndOrVerify() .onInputStream(ciphertextIn) .withOptions(ConsumerOptions.get().addDecryptionKey(removedKeys))); } From dfb7d068bd98e1c424511d7c3735a2426fbdd399 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Sat, 29 Oct 2022 15:50:34 +0200 Subject: [PATCH 76/80] Small clean-ups in OpenPgpMessageInputStream --- .../OpenPgpMessageInputStream.java | 108 +++++++----------- 1 file changed, 43 insertions(+), 65 deletions(-) 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 a509cde2..f2c30ced 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 @@ -109,7 +109,7 @@ public class OpenPgpMessageInputStream extends DecryptionStream { * @throws PGPException in case of an OpenPGP error */ public static OpenPgpMessageInputStream create(@Nonnull InputStream inputStream, - @Nonnull ConsumerOptions options) + @Nonnull ConsumerOptions options) throws IOException, PGPException { return create(inputStream, options, PGPainless.getPolicy()); } @@ -127,8 +127,8 @@ public class OpenPgpMessageInputStream extends DecryptionStream { * @throws IOException in case of an IO error */ public static OpenPgpMessageInputStream create(@Nonnull InputStream inputStream, - @Nonnull ConsumerOptions options, - @Nonnull Policy policy) + @Nonnull ConsumerOptions options, + @Nonnull Policy policy) throws PGPException, IOException { return create(inputStream, options, new MessageMetadata.Message(), policy); } @@ -479,9 +479,9 @@ public class OpenPgpMessageInputStream extends DecryptionStream { for (Passphrase passphrase : options.getDecryptionPassphrases()) { for (PGPPBEEncryptedData skesk : esks.skesks) { LOGGER.debug("Attempt decryption with provided passphrase"); - SymmetricKeyAlgorithm kekAlgorithm = SymmetricKeyAlgorithm.requireFromId(skesk.getAlgorithm()); + SymmetricKeyAlgorithm encapsulationAlgorithm = SymmetricKeyAlgorithm.requireFromId(skesk.getAlgorithm()); try { - throwIfUnacceptable(kekAlgorithm); + throwIfUnacceptable(encapsulationAlgorithm); } catch (UnacceptableAlgorithmException e) { LOGGER.debug("Skipping SKESK with unacceptable encapsulation algorithm", e); continue; @@ -489,7 +489,6 @@ public class OpenPgpMessageInputStream extends DecryptionStream { PBEDataDecryptorFactory decryptorFactory = ImplementationFactory.getInstance() .getPBEDataDecryptorFactory(passphrase); - if (decryptSKESKAndStream(skesk, decryptorFactory)) { return true; } @@ -497,10 +496,11 @@ public class OpenPgpMessageInputStream extends DecryptionStream { } List> postponedDueToMissingPassphrase = new ArrayList<>(); + // Try (known) secret keys for (PGPPublicKeyEncryptedData pkesk : esks.pkesks) { long keyId = pkesk.getKeyID(); - LOGGER.debug("Encountered PKESK with recipient " + KeyIdUtil.formatKeyId(keyId)); + LOGGER.debug("Encountered PKESK for recipient " + KeyIdUtil.formatKeyId(keyId)); PGPSecretKeyRing decryptionKeys = getDecryptionKey(keyId); if (decryptionKeys == null) { LOGGER.debug("Skipping PKESK because no matching key " + KeyIdUtil.formatKeyId(keyId) + " was provided"); @@ -508,13 +508,8 @@ public class OpenPgpMessageInputStream extends DecryptionStream { } PGPSecretKey secretKey = decryptionKeys.getSecretKey(keyId); SubkeyIdentifier decryptionKeyId = new SubkeyIdentifier(decryptionKeys, secretKey.getKeyID()); - S2K s2K = secretKey.getS2K(); - if (s2K != null) { - int s2kType = s2K.getType(); - if (s2kType >= 100 && s2kType <= 110) { - LOGGER.debug("Skipping PKESK because key " + decryptionKeyId + " has unsupported private S2K specifier " + s2kType); - continue; - } + if (hasUnsupportedS2KSpecifier(secretKey, decryptionKeyId)) { + continue; } LOGGER.debug("Attempt decryption using secret key " + decryptionKeyId); @@ -527,10 +522,7 @@ public class OpenPgpMessageInputStream extends DecryptionStream { } PGPPrivateKey privateKey = UnlockSecretKey.unlockSecretKey(secretKey, protector); - - PublicKeyDataDecryptorFactory decryptorFactory = ImplementationFactory.getInstance() - .getPublicKeyDataDecryptorFactory(privateKey); - if (decryptPKESKAndStream(decryptionKeyId, decryptorFactory, pkesk)) { + if (decryptWithPrivateKey(privateKey, decryptionKeyId, pkesk)) { return true; } } @@ -541,13 +533,8 @@ public class OpenPgpMessageInputStream extends DecryptionStream { PGPSecretKeyRing decryptionKeys = decryptionKeyCandidate.getA(); PGPSecretKey secretKey = decryptionKeyCandidate.getB(); SubkeyIdentifier decryptionKeyId = new SubkeyIdentifier(decryptionKeys, secretKey.getKeyID()); - S2K s2K = secretKey.getS2K(); - if (s2K != null) { - int s2kType = s2K.getType(); - if (s2kType >= 100 && s2kType <= 110) { - LOGGER.debug("Skipping PKESK because key " + decryptionKeyId + " has unsupported private S2K specifier " + s2kType); - continue; - } + if (hasUnsupportedS2KSpecifier(secretKey, decryptionKeyId)) { + continue; } LOGGER.debug("Attempt decryption of anonymous PKESK with key " + decryptionKeyId); SecretKeyRingProtector protector = options.getSecretKeyProtector(decryptionKeyCandidate.getA()); @@ -556,11 +543,9 @@ public class OpenPgpMessageInputStream extends DecryptionStream { postponedDueToMissingPassphrase.add(new Tuple<>(secretKey, pkesk)); continue; } - PGPPrivateKey privateKey = UnlockSecretKey.unlockSecretKey(decryptionKeyCandidate.getB(), protector); - PublicKeyDataDecryptorFactory decryptorFactory = ImplementationFactory.getInstance() - .getPublicKeyDataDecryptorFactory(privateKey); - if (decryptPKESKAndStream(decryptionKeyId, decryptorFactory, pkesk)) { + PGPPrivateKey privateKey = UnlockSecretKey.unlockSecretKey(secretKey, protector); + if (decryptWithPrivateKey(privateKey, decryptionKeyId, pkesk)) { return true; } } @@ -571,7 +556,8 @@ public class OpenPgpMessageInputStream extends DecryptionStream { Set keyIds = new HashSet<>(); for (Tuple k : postponedDueToMissingPassphrase) { PGPSecretKey key = k.getA(); - keyIds.add(new SubkeyIdentifier(getDecryptionKey(key.getKeyID()), key.getKeyID())); + PGPSecretKeyRing keys = getDecryptionKey(key.getKeyID()); + keyIds.add(new SubkeyIdentifier(keys, key.getKeyID())); } if (!keyIds.isEmpty()) { throw new MissingPassphraseException(keyIds); @@ -584,21 +570,14 @@ public class OpenPgpMessageInputStream extends DecryptionStream { long keyId = secretKey.getKeyID(); PGPSecretKeyRing decryptionKey = getDecryptionKey(keyId); SubkeyIdentifier decryptionKeyId = new SubkeyIdentifier(decryptionKey, keyId); - S2K s2K = secretKey.getS2K(); - if (s2K != null) { - int s2kType = s2K.getType(); - if (s2kType >= 100 && s2kType <= 110) { - LOGGER.debug("Skipping PKESK because key " + decryptionKeyId + " has unsupported private S2K specifier " + s2kType); - continue; - } + if (hasUnsupportedS2KSpecifier(secretKey, decryptionKeyId)) { + continue; } + LOGGER.debug("Attempt decryption with key " + decryptionKeyId + " while interactively requesting its passphrase"); SecretKeyRingProtector protector = options.getSecretKeyProtector(decryptionKey); - PGPPrivateKey privateKey = UnlockSecretKey.unlockSecretKey(secretKey, protector.getDecryptor(keyId)); - PublicKeyDataDecryptorFactory decryptorFactory = ImplementationFactory.getInstance() - .getPublicKeyDataDecryptorFactory(privateKey); - - if (decryptPKESKAndStream(decryptionKeyId, decryptorFactory, pkesk)) { + PGPPrivateKey privateKey = UnlockSecretKey.unlockSecretKey(secretKey, protector); + if (decryptWithPrivateKey(privateKey, decryptionKeyId, pkesk)) { return true; } } @@ -613,6 +592,27 @@ public class OpenPgpMessageInputStream extends DecryptionStream { return false; } + private boolean decryptWithPrivateKey(PGPPrivateKey privateKey, + SubkeyIdentifier decryptionKeyId, + PGPPublicKeyEncryptedData pkesk) + throws PGPException, IOException { + PublicKeyDataDecryptorFactory decryptorFactory = ImplementationFactory.getInstance() + .getPublicKeyDataDecryptorFactory(privateKey); + return decryptPKESKAndStream(decryptionKeyId, decryptorFactory, pkesk); + } + + private static boolean hasUnsupportedS2KSpecifier(PGPSecretKey secretKey, SubkeyIdentifier decryptionKeyId) { + S2K s2K = secretKey.getS2K(); + if (s2K != null) { + int s2kType = s2K.getType(); + if (s2kType >= 100 && s2kType <= 110) { + LOGGER.debug("Skipping PKESK because key " + decryptionKeyId + " has unsupported private S2K specifier " + s2kType); + return true; + } + } + return false; + } + private boolean decryptSKESKAndStream(PGPPBEEncryptedData skesk, PBEDataDecryptorFactory decryptorFactory) throws IOException, UnacceptableAlgorithmException { try { @@ -661,14 +661,6 @@ public class OpenPgpMessageInputStream extends DecryptionStream { return false; } - private PGPSecretKey getDecryptionKey(PGPSecretKeyRing decryptionKeys, long keyId) { - KeyRingInfo info = PGPainless.inspectKeyRing(decryptionKeys); - if (info.getEncryptionSubkeys(EncryptionPurpose.ANY).contains(info.getPublicKey(keyId))) { - return info.getSecretKey(keyId); - } - return null; - } - private void throwIfUnacceptable(SymmetricKeyAlgorithm algorithm) throws UnacceptableAlgorithmException { if (!policy.getSymmetricKeyDecryptionAlgorithmPolicy().isAcceptable(algorithm)) { @@ -883,20 +875,6 @@ public class OpenPgpMessageInputStream extends DecryptionStream { return resultBuilder.build(); } - static void log(String message) { - LOGGER.debug(message); - // CHECKSTYLE:OFF - System.out.println(message); - // CHECKSTYLE:ON - } - - static void log(String message, Throwable e) { - log(message); - // CHECKSTYLE:OFF - e.printStackTrace(); - // CHECKSTYLE:ON - } - // In 'OPS LIT("Foo") SIG', OPS is only updated with "Foo" // In 'OPS[1] OPS LIT("Foo") SIG SIG', OPS[1] (nested) is updated with OPS LIT("Foo") SIG. // Therefore, we need to handle the innermost signature layer differently when updating with Literal data. @@ -1004,7 +982,7 @@ public class OpenPgpMessageInputStream extends DecryptionStream { try { SignatureValidator.signatureWasCreatedInBounds(options.getVerifyNotBefore(), options.getVerifyNotAfter()) - .verify(signature); + .verify(signature); CertificateValidator.validateCertificateAndVerifyOnePassSignature(onePassSignature, policy); LOGGER.debug("Acceptable signature by key " + verification.getSigningKey()); layer.addVerifiedOnePassSignature(verification); From c20e80f716347fced64d297fb6a4fbe84dd53c45 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 2 Nov 2022 10:37:24 +0100 Subject: [PATCH 77/80] Implement efficient read(buf,off,len) for DelayedInputStream --- .../decryption_verification/TeeBCPGInputStream.java | 4 ---- 1 file changed, 4 deletions(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/TeeBCPGInputStream.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/TeeBCPGInputStream.java index 52ec9001..28b415e0 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/TeeBCPGInputStream.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/TeeBCPGInputStream.java @@ -127,9 +127,6 @@ public class TeeBCPGInputStream { } } - // TODO: Uncomment, once BC-172.1 is available - // see https://github.com/bcgit/bc-java/issues/1257 - /* @Override public int read(byte[] b, int off, int len) throws IOException { if (last != -1) { @@ -145,7 +142,6 @@ public class TeeBCPGInputStream { } return r; } - */ /** * Squeeze the last byte out and update the output stream. From cdef93597461dbe02801ab318941350605d7db1d Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 3 Nov 2022 12:07:26 +0100 Subject: [PATCH 78/80] Add comment for ArmorUtils method --- .../src/main/java/org/pgpainless/util/ArmorUtils.java | 1 + 1 file changed, 1 insertion(+) diff --git a/pgpainless-core/src/main/java/org/pgpainless/util/ArmorUtils.java b/pgpainless-core/src/main/java/org/pgpainless/util/ArmorUtils.java index 8a51ff3d..b3e60023 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/util/ArmorUtils.java +++ b/pgpainless-core/src/main/java/org/pgpainless/util/ArmorUtils.java @@ -178,6 +178,7 @@ public final class ArmorUtils { * If it is
false
, the signature will be encoded as-is. * * @param signature signature + * @param export whether to exclude non-exportable subpackets or trust-packets. * @return ascii armored string * * @throws IOException in case of an error in the {@link ArmoredOutputStream} From e2fc9ccb9c406458681bbed74ad4761b2aada31f Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 3 Nov 2022 12:29:50 +0100 Subject: [PATCH 79/80] Update changelog --- CHANGELOG.md | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 89adc4a7..3312c16e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,35 @@ SPDX-License-Identifier: CC0-1.0 # PGPainless Changelog +## 1.4.0-SNAPSHOT +- Reimplement message consumption via new `OpenPgpMessageInputStream` + - Fix validation of prepended signatures (#314) + - Fix validation of nested signatures (#319) + - Reject malformed messages (#237) + - Utilize new `PDA` syntax verifier class + - Allow for custom message syntax via `Syntax` class + - Gracefully handle `UnsupportedPacketVersionException` for signatures + - Allow plugin decryption code (e.g. to add support for hardware-backed keys (see #318)) + - Add `HardwareSecurity` utility class + - Add `GnuPGDummyKeyUtil` which can be used to mimic GnuPGs proprietary S2K extensions + for keys which were placed on hardware tokens + - Add `OpenPgpPacket` enum class to enumerate available packet tags + - Remove old decryption classes in favor of new implementation + - Removed `DecryptionStream` class and replaced with new abstract class + - Removed `DecryptionStreamFactory` + - Removed `FinalIOException` + - Removed `MissingLiteralDataException` (replaced by `MalformedOpenPgpMessageException`) + - Introduce `MessageMetadata` class as potential future replacement for `OpenPgpMetadata`. + - can be obtained via `((OpenPgpMessageInputStream) decryptionStream).getMetadata();` +- Add `CachingBcPublicKeyDataDecryptorFactory` which can be extended to prevent costly decryption + of session keys +- Fix: Only verify message integrity once +- Remove unnecessary `@throws` declarations on `KeyRingReader` methods +- Remove unnecessary `@throws` declarations on `KeyRingUtils` methods +- Add `KeyIdUtil.formatKeyId(long id)` to format hexadecimal key-ids. +- Add `KeyRingUtils.publicKeys(PGPKeyRing keys)` +- Remove `BCUtil` class + ## 1.3.8 - Bump `bcprov` to `1.72` - Bump `bcpg` to `1.72.1` From b216fd9d10ba58f04fdaf0509bd17c8a40e6ede5 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 3 Nov 2022 12:34:52 +0100 Subject: [PATCH 80/80] PGPainless 1.4.0-rc1 --- CHANGELOG.md | 2 +- README.md | 2 +- pgpainless-sop/README.md | 4 ++-- version.gradle | 4 ++-- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3312c16e..7817f8a5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ SPDX-License-Identifier: CC0-1.0 # PGPainless Changelog -## 1.4.0-SNAPSHOT +## 1.4.0-rc1 - Reimplement message consumption via new `OpenPgpMessageInputStream` - Fix validation of prepended signatures (#314) - Fix validation of nested signatures (#319) diff --git a/README.md b/README.md index 305e5cea..1855f742 100644 --- a/README.md +++ b/README.md @@ -191,7 +191,7 @@ repositories { } dependencies { - implementation 'org.pgpainless:pgpainless-core:1.3.8' + implementation 'org.pgpainless:pgpainless-core:1.4.0-rc1' } ``` diff --git a/pgpainless-sop/README.md b/pgpainless-sop/README.md index ebb1736c..ffd6c84b 100644 --- a/pgpainless-sop/README.md +++ b/pgpainless-sop/README.md @@ -23,7 +23,7 @@ To start using pgpainless-sop in your code, include the following lines in your ... dependencies { ... - implementation "org.pgpainless:pgpainless-sop:1.3.8" + implementation "org.pgpainless:pgpainless-sop:1.4.0-rc1" ... } @@ -34,7 +34,7 @@ dependencies { org.pgpainless pgpainless-sop - 1.3.8 + 1.4.0-rc1 ... diff --git a/version.gradle b/version.gradle index 1069132c..a756a407 100644 --- a/version.gradle +++ b/version.gradle @@ -4,8 +4,8 @@ allprojects { ext { - shortVersion = '1.4.0' - isSnapshot = true + shortVersion = '1.4.0-rc1' + isSnapshot = false pgpainlessMinAndroidSdk = 10 javaSourceCompatibility = 1.8 bouncyCastleVersion = '1.72'