From e86062c427dde86b3d75c5af2b3aa1f75c6b11a1 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 13 Sep 2022 19:23:59 +0200 Subject: [PATCH] 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()); + } + +}