package com.fsck.k9.message;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.annotation.VisibleForTesting;
import timber.log.Timber;
import com.fsck.k9.Globals;
import com.fsck.k9.K9;
import com.fsck.k9.activity.compose.ComposeCryptoStatus;
import com.fsck.k9.mail.Body;
import com.fsck.k9.mail.BodyPart;
import com.fsck.k9.mail.BoundaryGenerator;
import com.fsck.k9.mail.MessagingException;
import com.fsck.k9.mail.filter.EOLConvertingOutputStream;
import com.fsck.k9.mail.internet.BinaryTempFileBody;
import com.fsck.k9.mail.internet.MessageIdGenerator;
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.MimeUtility;
import com.fsck.k9.mail.internet.TextBody;
import com.fsck.k9.mailstore.BinaryMemoryBody;
import org.apache.commons.io.IOUtils;
import org.apache.james.mime4j.util.MimeUtil;
import org.openintents.openpgp.OpenPgpError;
import org.openintents.openpgp.util.OpenPgpApi;
import org.openintents.openpgp.util.OpenPgpApi.OpenPgpDataSource;
public class PgpMessageBuilder extends MessageBuilder {
private static final int REQUEST_USER_INTERACTION = 1;
private OpenPgpApi openPgpApi;
private MimeMessage currentProcessedMimeMessage;
private ComposeCryptoStatus cryptoStatus;
public static PgpMessageBuilder newInstance() {
Context context = Globals.getContext();
MessageIdGenerator messageIdGenerator = MessageIdGenerator.getInstance();
BoundaryGenerator boundaryGenerator = BoundaryGenerator.getInstance();
return new PgpMessageBuilder(context, messageIdGenerator, boundaryGenerator);
}
@VisibleForTesting
PgpMessageBuilder(Context context, MessageIdGenerator messageIdGenerator, BoundaryGenerator boundaryGenerator) {
super(context, messageIdGenerator, boundaryGenerator);
}
public void setOpenPgpApi(OpenPgpApi openPgpApi) {
this.openPgpApi = openPgpApi;
}
@Override
protected void buildMessageInternal() {
if (currentProcessedMimeMessage != null) {
throw new IllegalStateException("message can only be built once!");
}
if (cryptoStatus == null) {
throw new IllegalStateException("PgpMessageBuilder must have cryptoStatus set before building!");
}
if (cryptoStatus.isCryptoDisabled()) {
throw new AssertionError("PgpMessageBuilder must not be used if crypto is disabled!");
}
try {
if (!cryptoStatus.isProviderStateOk()) {
throw new MessagingException("OpenPGP Provider is not ready!");
}
currentProcessedMimeMessage = build();
} catch (MessagingException me) {
queueMessageBuildException(me);
return;
}
startOrContinueBuildMessage(null);
}
@Override
public void buildMessageOnActivityResult(int requestCode, @NonNull Intent userInteractionResult) {
if (currentProcessedMimeMessage == null) {
throw new AssertionError("build message from activity result must not be called individually");
}
startOrContinueBuildMessage(userInteractionResult);
}
private void startOrContinueBuildMessage(@Nullable Intent pgpApiIntent) {
try {
boolean shouldSign = cryptoStatus.isSigningEnabled();
boolean shouldEncrypt = cryptoStatus.isEncryptionEnabled();
boolean isPgpInlineMode = cryptoStatus.isPgpInlineModeEnabled();
if (!shouldSign && !shouldEncrypt) {
return;
}
boolean isSimpleTextMessage =
MimeUtility.isSameMimeType("text/plain", currentProcessedMimeMessage.getMimeType());
if (isPgpInlineMode && !isSimpleTextMessage) {
throw new MessagingException("Attachments are not supported in PGP/INLINE format!");
}
if (pgpApiIntent == null) {
pgpApiIntent = buildOpenPgpApiIntent(shouldSign, shouldEncrypt, isPgpInlineMode);
}
PendingIntent returnedPendingIntent = launchOpenPgpApiIntent(
pgpApiIntent, shouldEncrypt || isPgpInlineMode, shouldEncrypt || !isPgpInlineMode, isPgpInlineMode);
if (returnedPendingIntent != null) {
queueMessageBuildPendingIntent(returnedPendingIntent, REQUEST_USER_INTERACTION);
return;
}
queueMessageBuildSuccess(currentProcessedMimeMessage);
} catch (MessagingException me) {
queueMessageBuildException(me);
}
}
@NonNull
private Intent buildOpenPgpApiIntent(boolean shouldSign, boolean shouldEncrypt, boolean isPgpInlineMode)
throws MessagingException {
Intent pgpApiIntent;
if (shouldEncrypt) {
if (!shouldSign) {
throw new IllegalStateException("encrypt-only is not supported at this point and should never happen!");
}
// pgpApiIntent = new Intent(shouldSign ? OpenPgpApi.ACTION_SIGN_AND_ENCRYPT : OpenPgpApi.ACTION_ENCRYPT);
pgpApiIntent = new Intent(OpenPgpApi.ACTION_SIGN_AND_ENCRYPT);
long[] encryptKeyIds = cryptoStatus.getEncryptKeyIds();
if (encryptKeyIds != null) {
pgpApiIntent.putExtra(OpenPgpApi.EXTRA_KEY_IDS, encryptKeyIds);
}
if(!isDraft()) {
String[] encryptRecipientAddresses = cryptoStatus.getRecipientAddresses();
boolean hasRecipientAddresses = encryptRecipientAddresses != null && encryptRecipientAddresses.length > 0;
if (!hasRecipientAddresses) {
throw new MessagingException("encryption is enabled, but no recipient specified!");
}
pgpApiIntent.putExtra(OpenPgpApi.EXTRA_USER_IDS, encryptRecipientAddresses);
pgpApiIntent.putExtra(OpenPgpApi.EXTRA_ENCRYPT_OPPORTUNISTIC, cryptoStatus.isEncryptionOpportunistic());
}
} else {
pgpApiIntent = new Intent(isPgpInlineMode ? OpenPgpApi.ACTION_SIGN : OpenPgpApi.ACTION_DETACHED_SIGN);
}
if (shouldSign) {
pgpApiIntent.putExtra(OpenPgpApi.EXTRA_SIGN_KEY_ID, cryptoStatus.getSigningKeyId());
}
pgpApiIntent.putExtra(OpenPgpApi.EXTRA_REQUEST_ASCII_ARMOR, true);
return pgpApiIntent;
}
private PendingIntent launchOpenPgpApiIntent(@NonNull Intent openPgpIntent,
boolean captureOutputPart, boolean capturedOutputPartIs7Bit, boolean writeBodyContentOnly) throws MessagingException {
final MimeBodyPart bodyPart = currentProcessedMimeMessage.toBodyPart();
String[] contentType = currentProcessedMimeMessage.getHeader(MimeHeader.HEADER_CONTENT_TYPE);
if (contentType.length > 0) {
bodyPart.setHeader(MimeHeader.HEADER_CONTENT_TYPE, contentType[0]);
}
OpenPgpDataSource dataSource = createOpenPgpDataSourceFromBodyPart(bodyPart, writeBodyContentOnly);
BinaryTempFileBody pgpResultTempBody = null;
OutputStream outputStream = null;
if (captureOutputPart) {
try {
pgpResultTempBody = new BinaryTempFileBody(
capturedOutputPartIs7Bit ? MimeUtil.ENC_7BIT : MimeUtil.ENC_8BIT);
outputStream = pgpResultTempBody.getOutputStream();
// OpenKeychain/BouncyCastle at this point use the system newline for formatting, which is LF on android.
// we need this to be CRLF, so we convert the data after receiving.
outputStream = new EOLConvertingOutputStream(outputStream);
} catch (IOException e) {
throw new MessagingException("could not allocate temp file for storage!", e);
}
}
Intent result = openPgpApi.executeApi(openPgpIntent, dataSource, outputStream);
switch (result.getIntExtra(OpenPgpApi.RESULT_CODE, OpenPgpApi.RESULT_CODE_ERROR)) {
case OpenPgpApi.RESULT_CODE_SUCCESS:
mimeBuildMessage(result, bodyPart, pgpResultTempBody);
return null;
case OpenPgpApi.RESULT_CODE_USER_INTERACTION_REQUIRED:
PendingIntent returnedPendingIntent = result.getParcelableExtra(OpenPgpApi.RESULT_INTENT);
if (returnedPendingIntent == null) {
throw new MessagingException("openpgp api needs user interaction, but returned no pendingintent!");
}
return returnedPendingIntent;
case OpenPgpApi.RESULT_CODE_ERROR:
OpenPgpError error = result.getParcelableExtra(OpenPgpApi.RESULT_ERROR);
if (error == null) {
throw new MessagingException("internal openpgp api error");
}
boolean isOpportunisticError = error.getErrorId() == OpenPgpError.OPPORTUNISTIC_MISSING_KEYS;
if (isOpportunisticError) {
if (!cryptoStatus.isEncryptionOpportunistic()) {
throw new IllegalStateException(
"Got opportunistic error, but encryption wasn't supposed to be opportunistic!");
}
Timber.d("Skipping encryption due to opportunistic mode");
return null;
}
throw new MessagingException(error.getMessage());
}
throw new IllegalStateException("unreachable code segment reached");
}
@NonNull
private OpenPgpDataSource createOpenPgpDataSourceFromBodyPart(final MimeBodyPart bodyPart,
final boolean writeBodyContentOnly)
throws MessagingException {
return new OpenPgpDataSource() {
@Override
public void writeTo(OutputStream os) throws IOException {
try {
if (writeBodyContentOnly) {
Body body = bodyPart.getBody();
InputStream inputStream = body.getInputStream();
IOUtils.copy(inputStream, os);
} else {
bodyPart.writeTo(os);
}
} catch (MessagingException e) {
throw new IOException(e);
}
}
};
}
private void mimeBuildMessage(
@NonNull Intent result, @NonNull MimeBodyPart bodyPart, @Nullable BinaryTempFileBody pgpResultTempBody)
throws MessagingException {
if (pgpResultTempBody == null) {
boolean shouldHaveResultPart = cryptoStatus.isPgpInlineModeEnabled() || cryptoStatus.isEncryptionEnabled();
if (shouldHaveResultPart) {
throw new AssertionError("encryption or pgp/inline is enabled, but no output part!");
}
mimeBuildSignedMessage(bodyPart, result);
return;
}
if (cryptoStatus.isPgpInlineModeEnabled()) {
mimeBuildInlineMessage(pgpResultTempBody);
return;
}
mimeBuildEncryptedMessage(pgpResultTempBody);
}
private void mimeBuildSignedMessage(@NonNull BodyPart signedBodyPart, Intent result) throws MessagingException {
if (!cryptoStatus.isSigningEnabled()) {
throw new IllegalStateException("call to mimeBuildSignedMessage while signing isn't enabled!");
}
byte[] signedData = result.getByteArrayExtra(OpenPgpApi.RESULT_DETACHED_SIGNATURE);
if (signedData == null) {
throw new MessagingException("didn't find expected RESULT_DETACHED_SIGNATURE in api call result");
}
MimeMultipart multipartSigned = createMimeMultipart();
multipartSigned.setSubType("signed");
multipartSigned.addBodyPart(signedBodyPart);
multipartSigned.addBodyPart(
new MimeBodyPart(new BinaryMemoryBody(signedData, MimeUtil.ENC_7BIT),
"application/pgp-signature; name=\"signature.asc\""));
MimeMessageHelper.setBody(currentProcessedMimeMessage, multipartSigned);
String contentType = String.format(
"multipart/signed; boundary=\"%s\";\r\n protocol=\"application/pgp-signature\"",
multipartSigned.getBoundary());
if (result.hasExtra(OpenPgpApi.RESULT_SIGNATURE_MICALG)) {
String micAlgParameter = result.getStringExtra(OpenPgpApi.RESULT_SIGNATURE_MICALG);
contentType += String.format("; micalg=\"%s\"", micAlgParameter);
} else {
Timber.e("missing micalg parameter for pgp multipart/signed!");
}
currentProcessedMimeMessage.setHeader(MimeHeader.HEADER_CONTENT_TYPE, contentType);
}
private void mimeBuildEncryptedMessage(@NonNull Body encryptedBodyPart) throws MessagingException {
if (!cryptoStatus.isEncryptionEnabled()) {
throw new IllegalStateException("call to mimeBuildEncryptedMessage while encryption isn't enabled!");
}
MimeMultipart multipartEncrypted = createMimeMultipart();
multipartEncrypted.setSubType("encrypted");
multipartEncrypted.addBodyPart(new MimeBodyPart(new TextBody("Version: 1"), "application/pgp-encrypted"));
MimeBodyPart encryptedPart = new MimeBodyPart(encryptedBodyPart, "application/octet-stream; name=\"encrypted.asc\"");
encryptedPart.addHeader(MimeHeader.HEADER_CONTENT_DISPOSITION, "inline; filename=\"encrypted.asc\"");
multipartEncrypted.addBodyPart(encryptedPart);
MimeMessageHelper.setBody(currentProcessedMimeMessage, multipartEncrypted);
String contentType = String.format(
"multipart/encrypted; boundary=\"%s\";\r\n protocol=\"application/pgp-encrypted\"",
multipartEncrypted.getBoundary());
currentProcessedMimeMessage.setHeader(MimeHeader.HEADER_CONTENT_TYPE, contentType);
}
private void mimeBuildInlineMessage(@NonNull Body inlineBodyPart) throws MessagingException {
if (!cryptoStatus.isPgpInlineModeEnabled()) {
throw new IllegalStateException("call to mimeBuildInlineMessage while pgp/inline isn't enabled!");
}
boolean isCleartextSignature = !cryptoStatus.isEncryptionEnabled();
if (isCleartextSignature) {
inlineBodyPart.setEncoding(MimeUtil.ENC_QUOTED_PRINTABLE);
}
MimeMessageHelper.setBody(currentProcessedMimeMessage, inlineBodyPart);
}
public void setCryptoStatus(ComposeCryptoStatus cryptoStatus) {
this.cryptoStatus = cryptoStatus;
}
}