package com.fsck.k9.crypto; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.util.ArrayList; import java.util.List; import java.util.Stack; import android.support.annotation.Nullable; import android.support.annotation.VisibleForTesting; import android.text.TextUtils; import com.fsck.k9.mail.Body; import com.fsck.k9.mail.BodyPart; import com.fsck.k9.mail.MessagingException; import com.fsck.k9.mail.Multipart; import com.fsck.k9.mail.Part; import com.fsck.k9.mail.internet.MessageExtractor; import com.fsck.k9.mail.internet.MimeBodyPart; import com.fsck.k9.mail.internet.MimeUtility; import com.fsck.k9.mailstore.CryptoResultAnnotation; import com.fsck.k9.ui.crypto.MessageCryptoAnnotations; import static com.fsck.k9.mail.internet.MimeUtility.isSameMimeType; public class MessageDecryptVerifier { private static final String MULTIPART_ENCRYPTED = "multipart/encrypted"; private static final String MULTIPART_SIGNED = "multipart/signed"; private static final String PROTOCOL_PARAMETER = "protocol"; private static final String APPLICATION_PGP_ENCRYPTED = "application/pgp-encrypted"; private static final String APPLICATION_PGP_SIGNATURE = "application/pgp-signature"; private static final String TEXT_PLAIN = "text/plain"; // APPLICATION/PGP is a special case which occurs from mutt. see http://www.mutt.org/doc/PGP-Notes.txt private static final String APPLICATION_PGP = "application/pgp"; private static final String PGP_INLINE_START_MARKER = "-----BEGIN PGP MESSAGE-----"; private static final String PGP_INLINE_SIGNED_START_MARKER = "-----BEGIN PGP SIGNED MESSAGE-----"; private static final int TEXT_LENGTH_FOR_INLINE_CHECK = 36; public static Part findPrimaryEncryptedOrSignedPart(Part part, List<Part> outputExtraParts) { if (isPartEncryptedOrSigned(part)) { return part; } Part foundPart; foundPart = findPrimaryPartInAlternative(part); if (foundPart != null) { return foundPart; } foundPart = findPrimaryPartInMixed(part, outputExtraParts); if (foundPart != null) { return foundPart; } return null; } @Nullable private static Part findPrimaryPartInMixed(Part part, List<Part> outputExtraParts) { Body body = part.getBody(); boolean isMultipartMixed = part.isMimeType("multipart/mixed") && body instanceof Multipart; if (!isMultipartMixed) { return null; } Multipart multipart = (Multipart) body; if (multipart.getCount() == 0) { return null; } BodyPart firstBodyPart = multipart.getBodyPart(0); Part foundPart; if (isPartEncryptedOrSigned(firstBodyPart)) { foundPart = firstBodyPart; } else { foundPart = findPrimaryPartInAlternative(firstBodyPart); } if (foundPart != null && outputExtraParts != null) { for (int i = 1; i < multipart.getCount(); i++) { outputExtraParts.add(multipart.getBodyPart(i)); } } return foundPart; } private static Part findPrimaryPartInAlternative(Part part) { Body body = part.getBody(); if (part.isMimeType("multipart/alternative") && body instanceof Multipart) { Multipart multipart = (Multipart) body; if (multipart.getCount() == 0) { return null; } BodyPart firstBodyPart = multipart.getBodyPart(0); if (isPartPgpInlineEncryptedOrSigned(firstBodyPart)) { return firstBodyPart; } } return null; } public static List<Part> findEncryptedParts(Part startPart) { List<Part> encryptedParts = new ArrayList<>(); Stack<Part> partsToCheck = new Stack<>(); partsToCheck.push(startPart); while (!partsToCheck.isEmpty()) { Part part = partsToCheck.pop(); Body body = part.getBody(); if (isPartMultipartEncrypted(part)) { encryptedParts.add(part); continue; } if (body instanceof Multipart) { Multipart multipart = (Multipart) body; for (int i = multipart.getCount() - 1; i >= 0; i--) { BodyPart bodyPart = multipart.getBodyPart(i); partsToCheck.push(bodyPart); } } } return encryptedParts; } public static List<Part> findSignedParts(Part startPart, MessageCryptoAnnotations messageCryptoAnnotations) { List<Part> signedParts = new ArrayList<>(); Stack<Part> partsToCheck = new Stack<>(); partsToCheck.push(startPart); while (!partsToCheck.isEmpty()) { Part part = partsToCheck.pop(); if (messageCryptoAnnotations.has(part)) { CryptoResultAnnotation resultAnnotation = messageCryptoAnnotations.get(part); MimeBodyPart replacementData = resultAnnotation.getReplacementData(); if (replacementData != null) { part = replacementData; } } Body body = part.getBody(); if (isPartMultipartSigned(part)) { signedParts.add(part); continue; } if (body instanceof Multipart) { Multipart multipart = (Multipart) body; for (int i = multipart.getCount() - 1; i >= 0; i--) { BodyPart bodyPart = multipart.getBodyPart(i); partsToCheck.push(bodyPart); } } } return signedParts; } public static List<Part> findPgpInlineParts(Part startPart) { List<Part> inlineParts = new ArrayList<>(); Stack<Part> partsToCheck = new Stack<>(); partsToCheck.push(startPart); while (!partsToCheck.isEmpty()) { Part part = partsToCheck.pop(); Body body = part.getBody(); if (isPartPgpInlineEncryptedOrSigned(part)) { inlineParts.add(part); continue; } if (body instanceof Multipart) { Multipart multipart = (Multipart) body; for (int i = multipart.getCount() - 1; i >= 0; i--) { BodyPart bodyPart = multipart.getBodyPart(i); partsToCheck.push(bodyPart); } } } return inlineParts; } public static byte[] getSignatureData(Part part) throws IOException, MessagingException { if (isPartMultipartSigned(part)) { Body body = part.getBody(); if (body instanceof Multipart) { Multipart multi = (Multipart) body; BodyPart signatureBody = multi.getBodyPart(1); if (isSameMimeType(signatureBody.getMimeType(), APPLICATION_PGP_SIGNATURE)) { ByteArrayOutputStream bos = new ByteArrayOutputStream(); signatureBody.getBody().writeTo(bos); return bos.toByteArray(); } } } return null; } private static boolean isPartEncryptedOrSigned(Part part) { return isPartMultipartEncrypted(part) || isPartMultipartSigned(part) || isPartPgpInlineEncryptedOrSigned(part); } private static boolean isPartMultipartSigned(Part part) { return isSameMimeType(part.getMimeType(), MULTIPART_SIGNED); } private static boolean isPartMultipartEncrypted(Part part) { return isSameMimeType(part.getMimeType(), MULTIPART_ENCRYPTED); } // TODO also guess by mime-type of contained part? public static boolean isPgpMimeEncryptedOrSignedPart(Part part) { String contentType = part.getContentType(); String protocolParameter = MimeUtility.getHeaderParameter(contentType, PROTOCOL_PARAMETER); boolean isPgpEncrypted = isSameMimeType(part.getMimeType(), MULTIPART_ENCRYPTED) && APPLICATION_PGP_ENCRYPTED.equalsIgnoreCase(protocolParameter); boolean isPgpSigned = isSameMimeType(part.getMimeType(), MULTIPART_SIGNED) && APPLICATION_PGP_SIGNATURE.equalsIgnoreCase(protocolParameter); return isPgpEncrypted || isPgpSigned; } @VisibleForTesting static boolean isPartPgpInlineEncryptedOrSigned(Part part) { if (!part.isMimeType(TEXT_PLAIN) && !part.isMimeType(APPLICATION_PGP)) { return false; } String text = MessageExtractor.getTextFromPart(part, TEXT_LENGTH_FOR_INLINE_CHECK); if (TextUtils.isEmpty(text)) { return false; } text = text.trim(); return text.startsWith(PGP_INLINE_START_MARKER) || text.startsWith(PGP_INLINE_SIGNED_START_MARKER); } public static boolean isPartPgpInlineEncrypted(@Nullable Part part) { if (part == null) { return false; } if (!part.isMimeType(TEXT_PLAIN) && !part.isMimeType(APPLICATION_PGP)) { return false; } String text = MessageExtractor.getTextFromPart(part, TEXT_LENGTH_FOR_INLINE_CHECK); if (TextUtils.isEmpty(text)) { return false; } text = text.trim(); return text.startsWith(PGP_INLINE_START_MARKER); } }