001// SPDX-FileCopyrightText: 2021 Paul Schaub <vanitasvitae@fsfe.org>
002//
003// SPDX-License-Identifier: Apache-2.0
004
005package org.pgpainless.decryption_verification;
006
007import java.io.ByteArrayInputStream;
008import java.io.IOException;
009import java.io.InputStream;
010import java.nio.charset.Charset;
011import java.util.ArrayList;
012import java.util.Collections;
013import java.util.List;
014
015import org.bouncycastle.openpgp.PGPCompressedData;
016import org.bouncycastle.openpgp.PGPEncryptedData;
017import org.bouncycastle.openpgp.PGPEncryptedDataList;
018import org.bouncycastle.openpgp.PGPException;
019import org.bouncycastle.openpgp.PGPLiteralData;
020import org.bouncycastle.openpgp.PGPObjectFactory;
021import org.bouncycastle.openpgp.PGPOnePassSignatureList;
022import org.bouncycastle.openpgp.PGPPBEEncryptedData;
023import org.bouncycastle.openpgp.PGPPublicKeyEncryptedData;
024import org.bouncycastle.openpgp.PGPUtil;
025import org.pgpainless.implementation.ImplementationFactory;
026import org.pgpainless.util.ArmorUtils;
027
028/**
029 * Inspect an OpenPGP message to determine IDs of its encryption keys or whether it is passphrase protected.
030 */
031public final class MessageInspector {
032
033    public static class EncryptionInfo {
034        private final List<Long> keyIds = new ArrayList<>();
035        private boolean isPassphraseEncrypted = false;
036        private boolean isSignedOnly = false;
037
038        /**
039         * Return a list of recipient key ids for whom the message is encrypted.
040         * @return recipient key ids
041         */
042        public List<Long> getKeyIds() {
043            return Collections.unmodifiableList(keyIds);
044        }
045
046        public boolean isPassphraseEncrypted() {
047            return isPassphraseEncrypted;
048        }
049
050        /**
051         * Return true, if the message is encrypted.
052         *
053         * @return true if encrypted
054         */
055        public boolean isEncrypted() {
056            return isPassphraseEncrypted || !keyIds.isEmpty();
057        }
058
059        /**
060         * Return true, if the message is not encrypted, but signed using {@link org.bouncycastle.openpgp.PGPOnePassSignature OnePassSignatures}.
061         *
062         * @return true if message is signed only
063         */
064        public boolean isSignedOnly() {
065            return isSignedOnly;
066        }
067    }
068
069    private MessageInspector() {
070
071    }
072
073    /**
074     * Parses parts of the provided OpenPGP message in order to determine which keys were used to encrypt it.
075     *
076     * @param message OpenPGP message
077     * @return encryption info
078     */
079    public static EncryptionInfo determineEncryptionInfoForMessage(String message) throws PGPException, IOException {
080        @SuppressWarnings("CharsetObjectCanBeUsed")
081        Charset charset = Charset.forName("UTF-8");
082        return determineEncryptionInfoForMessage(new ByteArrayInputStream(message.getBytes(charset)));
083    }
084
085    /**
086     * Parses parts of the provided OpenPGP message in order to determine which keys were used to encrypt it.
087     * Note: This method does not rewind the passed in Stream, so you might need to take care of that yourselves.
088     *
089     * @param dataIn openpgp message
090     * @return encryption information
091     */
092    public static EncryptionInfo determineEncryptionInfoForMessage(InputStream dataIn) throws IOException, PGPException {
093        InputStream decoded = ArmorUtils.getDecoderStream(dataIn);
094        EncryptionInfo info = new EncryptionInfo();
095
096        processMessage(decoded, info);
097
098        return info;
099    }
100
101    private static void processMessage(InputStream dataIn, EncryptionInfo info) throws PGPException, IOException {
102        PGPObjectFactory objectFactory = ImplementationFactory.getInstance().getPGPObjectFactory(dataIn);
103
104        Object next;
105        while ((next = objectFactory.nextObject()) != null) {
106            if (next instanceof PGPOnePassSignatureList) {
107                PGPOnePassSignatureList signatures = (PGPOnePassSignatureList) next;
108                if (!signatures.isEmpty()) {
109                    info.isSignedOnly = true;
110                    return;
111                }
112            }
113
114            if (next instanceof PGPEncryptedDataList) {
115                PGPEncryptedDataList encryptedDataList = (PGPEncryptedDataList) next;
116                for (PGPEncryptedData encryptedData : encryptedDataList) {
117                    if (encryptedData instanceof PGPPublicKeyEncryptedData) {
118                        PGPPublicKeyEncryptedData pubKeyEncryptedData = (PGPPublicKeyEncryptedData) encryptedData;
119                        info.keyIds.add(pubKeyEncryptedData.getKeyID());
120                    } else if (encryptedData instanceof PGPPBEEncryptedData) {
121                        info.isPassphraseEncrypted = true;
122                    }
123                }
124                // Data is encrypted, we cannot go deeper
125                return;
126            }
127
128            if (next instanceof PGPCompressedData) {
129                PGPCompressedData compressed = (PGPCompressedData) next;
130                InputStream decompressed = compressed.getDataStream();
131                InputStream decoded = PGPUtil.getDecoderStream(decompressed);
132                objectFactory = ImplementationFactory.getInstance().getPGPObjectFactory(decoded);
133            }
134
135            if (next instanceof PGPLiteralData) {
136                return;
137            }
138        }
139    }
140}