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}