package com.fsck.k9.crypto;
import java.util.ArrayList;
import java.util.List;
import com.fsck.k9.K9RobolectricTestRunner;
import com.fsck.k9.mail.BodyPart;
import com.fsck.k9.mail.Message;
import com.fsck.k9.mail.MessagingException;
import com.fsck.k9.mail.Multipart;
import com.fsck.k9.mail.Part;
import com.fsck.k9.mail.internet.MimeBodyPart;
import com.fsck.k9.mail.internet.MimeHeader;
import com.fsck.k9.mail.internet.MimeMessage;
import com.fsck.k9.mail.internet.MimeMessageHelper;
import com.fsck.k9.mail.internet.MimeMultipart;
import com.fsck.k9.mail.internet.TextBody;
import com.fsck.k9.ui.crypto.MessageCryptoAnnotations;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.annotation.Config;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.mock;
@RunWith(K9RobolectricTestRunner.class)
@Config(manifest = Config.NONE)
public class MessageDecryptVerifierTest {
private static final String MIME_TYPE_MULTIPART_ENCRYPTED = "multipart/encrypted";
private MessageCryptoAnnotations messageCryptoAnnotations = mock(MessageCryptoAnnotations.class);
private static final String PROTCOL_PGP_ENCRYPTED = "application/pgp-encrypted";
private static final String PGP_INLINE_DATA = "" +
"-----BEGIN PGP MESSAGE-----\n" +
"Header: Value\n" +
"\n" +
"base64base64base64base64\n" +
"-----END PGP MESSAGE-----\n";
@Test
public void findPrimaryCryptoPart_withSimplePgpInline() throws Exception {
List<Part> outputExtraParts = new ArrayList<>();
Message message = new MimeMessage();
MimeMessageHelper.setBody(message, new TextBody(PGP_INLINE_DATA));
Part cryptoPart = MessageDecryptVerifier.findPrimaryEncryptedOrSignedPart(message, outputExtraParts);
assertSame(message, cryptoPart);
}
@Test
public void findPrimaryCryptoPart_withMultipartAlternativeContainingPgpInline() throws Exception {
List<Part> outputExtraParts = new ArrayList<>();
BodyPart pgpInlinePart = bodypart("text/plain", PGP_INLINE_DATA);
Message message = messageFromBody(
multipart("alternative",
pgpInlinePart,
bodypart("text/html")
)
);
Part cryptoPart = MessageDecryptVerifier.findPrimaryEncryptedOrSignedPart(message, outputExtraParts);
assertSame(pgpInlinePart, cryptoPart);
}
@Test
public void findPrimaryCryptoPart_withMultipartMixedContainingPgpInline() throws Exception {
List<Part> outputExtraParts = new ArrayList<>();
BodyPart pgpInlinePart = bodypart("text/plain", PGP_INLINE_DATA);
Message message = messageFromBody(
multipart("mixed",
pgpInlinePart,
bodypart("application/octet-stream")
)
);
Part cryptoPart = MessageDecryptVerifier.findPrimaryEncryptedOrSignedPart(message, outputExtraParts);
assertSame(pgpInlinePart, cryptoPart);
}
@Test
public void findPrimaryCryptoPart_withMultipartMixedContainingMultipartAlternativeContainingPgpInline()
throws Exception {
List<Part> outputExtraParts = new ArrayList<>();
BodyPart pgpInlinePart = bodypart("text/plain", PGP_INLINE_DATA);
Message message = messageFromBody(
multipart("mixed",
multipart("alternative",
pgpInlinePart,
bodypart("text/html")
),
bodypart("application/octet-stream")
)
);
Part cryptoPart = MessageDecryptVerifier.findPrimaryEncryptedOrSignedPart(message, outputExtraParts);
assertSame(pgpInlinePart, cryptoPart);
}
@Test
public void findPrimaryCryptoPart_withEmptyMultipartAlternative_shouldReturnNull() throws Exception {
List<Part> outputExtraParts = new ArrayList<>();
Message message = messageFromBody(
multipart("alternative")
);
Part cryptoPart = MessageDecryptVerifier.findPrimaryEncryptedOrSignedPart(message, outputExtraParts);
assertNull(cryptoPart);
}
@Test
public void findPrimaryCryptoPart_withEmptyMultipartMixed_shouldReturnNull() throws Exception {
List<Part> outputExtraParts = new ArrayList<>();
Message message = messageFromBody(
multipart("mixed")
);
Part cryptoPart = MessageDecryptVerifier.findPrimaryEncryptedOrSignedPart(message, outputExtraParts);
assertNull(cryptoPart);
}
@Test
public void findPrimaryCryptoPart_withEmptyMultipartAlternativeInsideMultipartMixed_shouldReturnNull()
throws Exception {
List<Part> outputExtraParts = new ArrayList<>();
Message message = messageFromBody(
multipart("mixed",
multipart("alternative")
)
);
Part cryptoPart = MessageDecryptVerifier.findPrimaryEncryptedOrSignedPart(message, outputExtraParts);
assertNull(cryptoPart);
}
@Test
public void findEncryptedPartsShouldReturnEmptyListForEmptyMessage() throws Exception {
MimeMessage emptyMessage = new MimeMessage();
List<Part> encryptedParts = MessageDecryptVerifier.findEncryptedParts(emptyMessage);
assertEquals(0, encryptedParts.size());
}
@Test
public void findEncryptedPartsShouldReturnEmptyListForSimpleMessage() throws Exception {
MimeMessage message = new MimeMessage();
message.setBody(new TextBody("message text"));
List<Part> encryptedParts = MessageDecryptVerifier.findEncryptedParts(message);
assertEquals(0, encryptedParts.size());
}
@Test
public void findEncryptedPartsShouldReturnEmptyEncryptedPart() throws Exception {
MimeMessage message = new MimeMessage();
MimeMultipart multipartEncrypted = MimeMultipart.newInstance();
multipartEncrypted.setSubType("encrypted");
MimeMessageHelper.setBody(message, multipartEncrypted);
setContentTypeWithProtocol(message, MIME_TYPE_MULTIPART_ENCRYPTED, PROTCOL_PGP_ENCRYPTED);
List<Part> encryptedParts = MessageDecryptVerifier.findEncryptedParts(message);
assertEquals(1, encryptedParts.size());
assertSame(message, encryptedParts.get(0));
}
@Test
public void findEncryptedPartsShouldReturnMultipleEncryptedParts() throws Exception {
MimeMessage message = new MimeMessage();
MimeMultipart multipartMixed = MimeMultipart.newInstance();
multipartMixed.setSubType("mixed");
MimeMessageHelper.setBody(message, multipartMixed);
MimeMultipart multipartEncryptedOne = MimeMultipart.newInstance();
multipartEncryptedOne.setSubType("encrypted");
MimeBodyPart bodyPartOne = new MimeBodyPart(multipartEncryptedOne);
setContentTypeWithProtocol(bodyPartOne, MIME_TYPE_MULTIPART_ENCRYPTED, PROTCOL_PGP_ENCRYPTED);
multipartMixed.addBodyPart(bodyPartOne);
MimeBodyPart bodyPartTwo = new MimeBodyPart(null, "text/plain");
multipartMixed.addBodyPart(bodyPartTwo);
MimeMultipart multipartEncryptedThree = MimeMultipart.newInstance();
multipartEncryptedThree.setSubType("encrypted");
MimeBodyPart bodyPartThree = new MimeBodyPart(multipartEncryptedThree);
setContentTypeWithProtocol(bodyPartThree, MIME_TYPE_MULTIPART_ENCRYPTED, PROTCOL_PGP_ENCRYPTED);
multipartMixed.addBodyPart(bodyPartThree);
List<Part> encryptedParts = MessageDecryptVerifier.findEncryptedParts(message);
assertEquals(2, encryptedParts.size());
assertSame(bodyPartOne, encryptedParts.get(0));
assertSame(bodyPartThree, encryptedParts.get(1));
}
@Test
public void findEncrypted__withMultipartEncrypted__shouldReturnRoot() throws Exception {
Message message = messageFromBody(
multipart("encrypted",
bodypart("application/pgp-encrypted"),
bodypart("application/octet-stream")
)
);
List<Part> encryptedParts = MessageDecryptVerifier.findEncryptedParts(message);
assertEquals(1, encryptedParts.size());
assertSame(message, encryptedParts.get(0));
}
@Test
public void findEncrypted__withMultipartMixedSubEncrypted__shouldReturnRoot() throws Exception {
Message message = messageFromBody(
multipart("mixed",
multipart("encrypted",
bodypart("application/pgp-encrypted"),
bodypart("application/octet-stream")
)
)
);
List<Part> encryptedParts = MessageDecryptVerifier.findEncryptedParts(message);
assertEquals(1, encryptedParts.size());
assertSame(getPart(message, 0), encryptedParts.get(0));
}
@Test
public void findEncrypted__withMultipartMixedSubEncryptedAndEncrypted__shouldReturnBoth()
throws Exception {
Message message = messageFromBody(
multipart("mixed",
multipart("encrypted",
bodypart("application/pgp-encrypted"),
bodypart("application/octet-stream")
),
multipart("encrypted",
bodypart("application/pgp-encrypted"),
bodypart("application/octet-stream")
)
)
);
List<Part> encryptedParts = MessageDecryptVerifier.findEncryptedParts(message);
assertEquals(2, encryptedParts.size());
assertSame(getPart(message, 0), encryptedParts.get(0));
assertSame(getPart(message, 1), encryptedParts.get(1));
}
@Test
public void findEncrypted__withMultipartMixedSubTextAndEncrypted__shouldReturnEncrypted() throws Exception {
Message message = messageFromBody(
multipart("mixed",
bodypart("text/plain"),
multipart("encrypted",
bodypart("application/pgp-encrypted"),
bodypart("application/octet-stream")
)
)
);
List<Part> encryptedParts = MessageDecryptVerifier.findEncryptedParts(message);
assertEquals(1, encryptedParts.size());
assertSame(getPart(message, 1), encryptedParts.get(0));
}
@Test
public void findEncrypted__withMultipartMixedSubEncryptedAndText__shouldReturnEncrypted() throws Exception {
Message message = messageFromBody(
multipart("mixed",
multipart("encrypted",
bodypart("application/pgp-encrypted"),
bodypart("application/octet-stream")
),
bodypart("text/plain")
)
);
List<Part> encryptedParts = MessageDecryptVerifier.findEncryptedParts(message);
assertEquals(1, encryptedParts.size());
assertSame(getPart(message, 0), encryptedParts.get(0));
}
@Test
public void findSigned__withSimpleMultipartSigned__shouldReturnRoot() throws Exception {
Message message = messageFromBody(
multipart("signed",
bodypart("text/plain"),
bodypart("application/pgp-signature")
)
);
List<Part> signedParts = MessageDecryptVerifier.findSignedParts(message, messageCryptoAnnotations);
assertEquals(1, signedParts.size());
assertSame(message, signedParts.get(0));
}
@Test
public void findSigned__withComplexMultipartSigned__shouldReturnRoot() throws Exception {
Message message = messageFromBody(
multipart("signed",
multipart("mixed",
bodypart("text/plain"),
bodypart("application/pdf")
),
bodypart("application/pgp-signature")
)
);
List<Part> signedParts = MessageDecryptVerifier.findSignedParts(message, messageCryptoAnnotations);
assertEquals(1, signedParts.size());
assertSame(message, signedParts.get(0));
}
@Test
public void findEncrypted__withMultipartMixedSubSigned__shouldReturnSigned() throws Exception {
Message message = messageFromBody(
multipart("mixed",
multipart("signed",
bodypart("text/plain"),
bodypart("application/pgp-signature")
)
)
);
List<Part> signedParts = MessageDecryptVerifier.findSignedParts(message, messageCryptoAnnotations);
assertEquals(1, signedParts.size());
assertSame(getPart(message, 0), signedParts.get(0));
}
@Test
public void findEncrypted__withMultipartMixedSubSignedAndText__shouldReturnSigned() throws Exception {
Message message = messageFromBody(
multipart("mixed",
multipart("signed",
bodypart("text/plain"),
bodypart("application/pgp-signature")
),
bodypart("text/plain")
)
);
List<Part> signedParts = MessageDecryptVerifier.findSignedParts(message, messageCryptoAnnotations);
assertEquals(1, signedParts.size());
assertSame(getPart(message, 0), signedParts.get(0));
}
@Test
public void findEncrypted__withMultipartMixedSubTextAndSigned__shouldReturnSigned() throws Exception {
Message message = messageFromBody(
multipart("mixed",
bodypart("text/plain"),
multipart("signed",
bodypart("text/plain"),
bodypart("application/pgp-signature")
)
)
);
List<Part> signedParts = MessageDecryptVerifier.findSignedParts(message, messageCryptoAnnotations);
assertEquals(1, signedParts.size());
assertSame(getPart(message, 1), signedParts.get(0));
}
@Test
public void isPgpInlineMethods__withPgpInlineData__shouldReturnTrue() throws Exception {
String pgpInlineData = "-----BEGIN PGP MESSAGE-----\n" +
"Header: Value\n" +
"\n" +
"base64base64base64base64\n" +
"-----END PGP MESSAGE-----\n";
MimeMessage message = new MimeMessage();
message.setBody(new TextBody(pgpInlineData));
assertTrue(MessageDecryptVerifier.isPartPgpInlineEncrypted(message));
}
@Test
public void isPgpInlineMethods__withEncryptedDataAndLeadingWhitespace__shouldReturnTrue() throws Exception {
String pgpInlineData = "\n \n \n" +
"-----BEGIN PGP MESSAGE-----\n" +
"Header: Value\n" +
"\n" +
"base64base64base64base64\n" +
"-----END PGP MESSAGE-----\n";
MimeMessage message = new MimeMessage();
message.setBody(new TextBody(pgpInlineData));
assertTrue(MessageDecryptVerifier.isPartPgpInlineEncryptedOrSigned(message));
assertTrue(MessageDecryptVerifier.isPartPgpInlineEncrypted(message));
}
@Test
public void isPgpInlineMethods__withEncryptedDataAndLeadingGarbage__shouldReturnFalse() throws Exception {
String pgpInlineData = "garbage!" +
"-----BEGIN PGP MESSAGE-----\n" +
"Header: Value\n" +
"\n" +
"base64base64base64base64\n" +
"-----END PGP MESSAGE-----\n";
MimeMessage message = new MimeMessage();
message.setBody(new TextBody(pgpInlineData));
assertFalse(MessageDecryptVerifier.isPartPgpInlineEncryptedOrSigned(message));
assertFalse(MessageDecryptVerifier.isPartPgpInlineEncrypted(message));
}
@Test
public void isPartPgpInlineEncryptedOrSigned__withSignedData__shouldReturnTrue() throws Exception {
String pgpInlineData = "-----BEGIN PGP SIGNED MESSAGE-----\n" +
"Header: Value\n" +
"\n" +
"-----BEGIN PGP SIGNATURE-----\n" +
"Header: Value\n" +
"\n" +
"base64base64base64base64\n" +
"-----END PGP SIGNED MESSAGE-----\n";
MimeMessage message = new MimeMessage();
message.setBody(new TextBody(pgpInlineData));
assertTrue(MessageDecryptVerifier.isPartPgpInlineEncryptedOrSigned(message));
}
@Test
public void isPartPgpInlineEncrypted__withSignedData__shouldReturnFalse() throws Exception {
String pgpInlineData = "-----BEGIN PGP SIGNED MESSAGE-----\n" +
"Header: Value\n" +
"\n" +
"-----BEGIN PGP SIGNATURE-----\n" +
"Header: Value\n" +
"\n" +
"base64base64base64base64\n" +
"-----END PGP SIGNED MESSAGE-----\n";
MimeMessage message = new MimeMessage();
message.setBody(new TextBody(pgpInlineData));
assertFalse(MessageDecryptVerifier.isPartPgpInlineEncrypted(message));
}
MimeMessage messageFromBody(BodyPart bodyPart) throws MessagingException {
MimeMessage message = new MimeMessage();
MimeMessageHelper.setBody(message, bodyPart.getBody());
return message;
}
MimeBodyPart multipart(String type, BodyPart... subParts) throws MessagingException {
MimeMultipart multiPart = MimeMultipart.newInstance();
multiPart.setSubType(type);
for (BodyPart subPart : subParts) {
multiPart.addBodyPart(subPart);
}
return new MimeBodyPart(multiPart);
}
BodyPart bodypart(String type) throws MessagingException {
return new MimeBodyPart(null, type);
}
BodyPart bodypart(String type, String text) throws MessagingException {
TextBody textBody = new TextBody(text);
return new MimeBodyPart(textBody, type);
}
public static Part getPart(Part searchRootPart, int... indexes) {
Part part = searchRootPart;
for (int index : indexes) {
part = ((Multipart) part.getBody()).getBodyPart(index);
}
return part;
}
//TODO: Find a cleaner way to do this
private static void setContentTypeWithProtocol(Part part, String mimeType, String protocol)
throws MessagingException {
part.setHeader(MimeHeader.HEADER_CONTENT_TYPE, mimeType + "; protocol=\"" + protocol + "\"");
}
}