mirror of
https://github.com/moparisthebest/k-9
synced 2024-12-24 08:38:51 -05:00
First attempt at decrypting PGP/MIME messages
This commit is contained in:
parent
bb83fdc0e8
commit
7f811fce2c
@ -2,6 +2,7 @@
|
||||
package com.fsck.k9.mail;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
|
||||
public interface Part {
|
||||
|
@ -7,13 +7,15 @@ import java.util.Stack;
|
||||
|
||||
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.MimeUtility;
|
||||
|
||||
|
||||
public class MessageDecryptor {
|
||||
private static final String MULTIPART_ENCRYPTED = "multipart/encrypted";
|
||||
private static final String PROTOCOL_PARAMETER = "protocol";
|
||||
private static final String APPLICATION_PGP_ENCRYPTED = "application/pgp-encrypted";
|
||||
|
||||
public static List<Part> findEncryptedParts(Part startPart) {
|
||||
List<Part> encryptedParts = new ArrayList<Part>();
|
||||
@ -38,4 +40,12 @@ public class MessageDecryptor {
|
||||
|
||||
return encryptedParts;
|
||||
}
|
||||
|
||||
public static boolean isPgpMimeEncryptedPart(Part part) {
|
||||
//FIXME: Doesn't work right now because LocalMessage.getContentType() doesn't load headers from database
|
||||
// String contentType = part.getContentType();
|
||||
// String protocol = MimeUtility.getHeaderParameter(contentType, PROTOCOL_PARAMETER);
|
||||
// return APPLICATION_PGP_ENCRYPTED.equals(protocol);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,200 @@
|
||||
package com.fsck.k9.mailstore;
|
||||
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.Stack;
|
||||
|
||||
import com.fsck.k9.mail.Body;
|
||||
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.MimeMessage;
|
||||
import com.fsck.k9.mail.internet.MimeMultipart;
|
||||
import com.fsck.k9.mail.internet.MimeUtility;
|
||||
import org.apache.commons.io.IOUtils;
|
||||
import org.apache.james.mime4j.MimeException;
|
||||
import org.apache.james.mime4j.io.EOLConvertingInputStream;
|
||||
import org.apache.james.mime4j.parser.ContentHandler;
|
||||
import org.apache.james.mime4j.parser.MimeStreamParser;
|
||||
import org.apache.james.mime4j.stream.BodyDescriptor;
|
||||
import org.apache.james.mime4j.stream.Field;
|
||||
import org.apache.james.mime4j.stream.MimeConfig;
|
||||
|
||||
|
||||
public class DecryptStreamParser {
|
||||
public static DecryptedBodyPart parse(Part multipartEncrypted, InputStream inputStream) throws MessagingException, IOException {
|
||||
DecryptedBodyPart decryptedRootPart = new DecryptedBodyPart(multipartEncrypted);
|
||||
|
||||
MimeConfig parserConfig = new MimeConfig();
|
||||
parserConfig.setMaxHeaderLen(-1);
|
||||
parserConfig.setMaxLineLen(-1);
|
||||
parserConfig.setMaxHeaderCount(-1);
|
||||
|
||||
MimeStreamParser parser = new MimeStreamParser(parserConfig);
|
||||
parser.setContentHandler(new PartBuilder(multipartEncrypted, decryptedRootPart));
|
||||
parser.setRecurse();
|
||||
|
||||
try {
|
||||
parser.parse(new EOLConvertingInputStream(inputStream));
|
||||
} catch (MimeException e) {
|
||||
throw new MessagingException("Failed to parse decrypted content", e);
|
||||
}
|
||||
|
||||
return decryptedRootPart;
|
||||
}
|
||||
|
||||
private static Body createBody(InputStream inputStream, String transferEncoding) throws IOException {
|
||||
//TODO: only read parts we're going to display into memory
|
||||
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
|
||||
try {
|
||||
IOUtils.copy(inputStream, byteArrayOutputStream);
|
||||
} finally {
|
||||
byteArrayOutputStream.close();
|
||||
}
|
||||
|
||||
byte[] data = byteArrayOutputStream.toByteArray();
|
||||
|
||||
return new BinaryMemoryBody(data, transferEncoding);
|
||||
}
|
||||
|
||||
|
||||
private static class PartBuilder implements ContentHandler {
|
||||
private final Part multipartEncrypted;
|
||||
private final DecryptedBodyPart decryptedRootPart;
|
||||
private final Stack<Object> stack = new Stack<Object>();
|
||||
|
||||
public PartBuilder(Part multipartEncrypted, DecryptedBodyPart decryptedRootPart) throws MessagingException {
|
||||
this.multipartEncrypted = multipartEncrypted;
|
||||
this.decryptedRootPart = decryptedRootPart;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void startMessage() throws MimeException {
|
||||
if (stack.isEmpty()) {
|
||||
stack.push(decryptedRootPart);
|
||||
} else {
|
||||
Part part = (Part) stack.peek();
|
||||
|
||||
Message innerMessage = new DecryptedMimeMessage(multipartEncrypted);
|
||||
part.setBody(innerMessage);
|
||||
|
||||
stack.push(innerMessage);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void endMessage() throws MimeException {
|
||||
stack.pop();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void startBodyPart() throws MimeException {
|
||||
try {
|
||||
Multipart multipart = (Multipart) stack.peek();
|
||||
|
||||
BodyPart bodyPart = new DecryptedBodyPart(multipartEncrypted);
|
||||
multipart.addBodyPart(bodyPart);
|
||||
|
||||
stack.push(bodyPart);
|
||||
} catch (MessagingException e) {
|
||||
throw new MimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void endBodyPart() throws MimeException {
|
||||
stack.pop();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void startHeader() throws MimeException {
|
||||
// Do nothing
|
||||
}
|
||||
|
||||
@Override
|
||||
public void field(Field parsedField) throws MimeException {
|
||||
try {
|
||||
String name = parsedField.getName();
|
||||
String raw = parsedField.getRaw().toString();
|
||||
|
||||
Part part = (Part) stack.peek();
|
||||
part.addRawHeader(name, raw);
|
||||
} catch (MessagingException e) {
|
||||
throw new MimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void endHeader() throws MimeException {
|
||||
// Do nothing
|
||||
}
|
||||
|
||||
@Override
|
||||
public void preamble(InputStream is) throws MimeException, IOException {
|
||||
// Do nothing
|
||||
}
|
||||
|
||||
@Override
|
||||
public void epilogue(InputStream is) throws MimeException, IOException {
|
||||
// Do nothing
|
||||
}
|
||||
|
||||
@Override
|
||||
public void startMultipart(BodyDescriptor bd) throws MimeException {
|
||||
Part part = (Part) stack.peek();
|
||||
try {
|
||||
String contentType = part.getContentType();
|
||||
String mimeType = MimeUtility.getHeaderParameter(contentType, null);
|
||||
String boundary = MimeUtility.getHeaderParameter(contentType, "boundary");
|
||||
|
||||
MimeMultipart multipart = new MimeMultipart(mimeType, boundary);
|
||||
part.setBody(multipart);
|
||||
|
||||
stack.push(multipart);
|
||||
} catch (MessagingException e) {
|
||||
throw new MimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void endMultipart() throws MimeException {
|
||||
stack.pop();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void body(BodyDescriptor bd, InputStream inputStream) throws MimeException, IOException {
|
||||
Part part = (Part) stack.peek();
|
||||
|
||||
String transferEncoding = bd.getTransferEncoding();
|
||||
Body body = createBody(inputStream, transferEncoding);
|
||||
|
||||
part.setBody(body);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void raw(InputStream is) throws MimeException, IOException {
|
||||
throw new IllegalStateException("Not implemented");
|
||||
}
|
||||
}
|
||||
|
||||
public static class DecryptedBodyPart extends MimeBodyPart {
|
||||
private final Part multipartEncrypted;
|
||||
|
||||
public DecryptedBodyPart(Part multipartEncrypted) throws MessagingException {
|
||||
this.multipartEncrypted = multipartEncrypted;
|
||||
}
|
||||
}
|
||||
|
||||
public static class DecryptedMimeMessage extends MimeMessage {
|
||||
private final Part multipartEncrypted;
|
||||
|
||||
public DecryptedMimeMessage(Part multipartEncrypted) {
|
||||
this.multipartEncrypted = multipartEncrypted;
|
||||
}
|
||||
}
|
||||
}
|
@ -442,7 +442,8 @@ public class LocalMessageExtractor {
|
||||
|
||||
return new AttachmentViewInfo(mimeType, displayName, size, uri, firstClassAttachment, part);
|
||||
} else {
|
||||
throw new IllegalStateException("Not supported yet");
|
||||
//FIXME: The content provider URI thing needs to be reworked
|
||||
return extractAttachmentInfo(part, null);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,7 +1,13 @@
|
||||
package com.fsck.k9.ui.messageview;
|
||||
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.PipedInputStream;
|
||||
import java.io.PipedOutputStream;
|
||||
import java.util.ArrayDeque;
|
||||
import java.util.Collections;
|
||||
import java.util.Deque;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
import android.app.Activity;
|
||||
@ -14,6 +20,7 @@ import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.Loader;
|
||||
import android.net.Uri;
|
||||
import android.os.AsyncTask;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.text.TextUtils;
|
||||
@ -27,6 +34,7 @@ import android.view.ViewGroup;
|
||||
import android.widget.Toast;
|
||||
|
||||
import com.fsck.k9.Account;
|
||||
import com.fsck.k9.Identity;
|
||||
import com.fsck.k9.K9;
|
||||
import com.fsck.k9.Preferences;
|
||||
import com.fsck.k9.R;
|
||||
@ -34,21 +42,35 @@ import com.fsck.k9.activity.ChooseFolder;
|
||||
import com.fsck.k9.activity.MessageReference;
|
||||
import com.fsck.k9.controller.MessagingController;
|
||||
import com.fsck.k9.controller.MessagingListener;
|
||||
import com.fsck.k9.crypto.MessageDecryptor;
|
||||
import com.fsck.k9.crypto.OpenPgpApiHelper;
|
||||
import com.fsck.k9.crypto.PgpData;
|
||||
import com.fsck.k9.fragment.ConfirmationDialogFragment;
|
||||
import com.fsck.k9.fragment.ConfirmationDialogFragment.ConfirmationDialogFragmentListener;
|
||||
import com.fsck.k9.fragment.ProgressDialogFragment;
|
||||
import com.fsck.k9.helper.FileBrowserHelper;
|
||||
import com.fsck.k9.helper.FileBrowserHelper.FileBrowserFailOverCallback;
|
||||
import com.fsck.k9.helper.IdentityHelper;
|
||||
import com.fsck.k9.mail.Body;
|
||||
import com.fsck.k9.mail.BodyPart;
|
||||
import com.fsck.k9.mail.Flag;
|
||||
import com.fsck.k9.mail.MessagingException;
|
||||
import com.fsck.k9.mail.Multipart;
|
||||
import com.fsck.k9.mail.Part;
|
||||
import com.fsck.k9.mailstore.AttachmentViewInfo;
|
||||
import com.fsck.k9.mailstore.DecryptStreamParser;
|
||||
import com.fsck.k9.mailstore.DecryptStreamParser.DecryptedBodyPart;
|
||||
import com.fsck.k9.mailstore.LocalMessage;
|
||||
import com.fsck.k9.mailstore.MessageViewInfo;
|
||||
import com.fsck.k9.ui.message.DecodeMessageLoader;
|
||||
import com.fsck.k9.ui.message.LocalMessageLoader;
|
||||
import com.fsck.k9.view.MessageHeader;
|
||||
import org.openintents.openpgp.IOpenPgpService;
|
||||
import org.openintents.openpgp.OpenPgpSignatureResult;
|
||||
import org.openintents.openpgp.util.OpenPgpApi;
|
||||
import org.openintents.openpgp.util.OpenPgpApi.IOpenPgpCallback;
|
||||
import org.openintents.openpgp.util.OpenPgpServiceConnection;
|
||||
import org.openintents.openpgp.util.OpenPgpServiceConnection.OnBound;
|
||||
|
||||
|
||||
public class MessageViewFragment extends Fragment implements ConfirmationDialogFragmentListener,
|
||||
@ -109,6 +131,9 @@ public class MessageViewFragment extends Fragment implements ConfirmationDialogF
|
||||
private LoaderCallbacks<MessageViewInfo> decodeMessageLoaderCallback = new DecodeMessageLoaderCallback();
|
||||
private MessageViewInfo messageViewInfo;
|
||||
private AttachmentViewInfo currentAttachmentViewInfo;
|
||||
private Deque<Part> partsToDecrypt;
|
||||
private OpenPgpApi openPgpApi;
|
||||
private Part currentlyDecryptingPart;
|
||||
|
||||
|
||||
@Override
|
||||
@ -223,11 +248,177 @@ public class MessageViewFragment extends Fragment implements ConfirmationDialogF
|
||||
|
||||
if (message.isBodyMissing()) {
|
||||
startDownloadingMessageBody(message);
|
||||
} else {
|
||||
decryptMessagePartsIfNecessary(message);
|
||||
}
|
||||
}
|
||||
|
||||
private void decryptMessagePartsIfNecessary(LocalMessage message) {
|
||||
List<Part> encryptedParts = MessageDecryptor.findEncryptedParts(message);
|
||||
if (!encryptedParts.isEmpty()) {
|
||||
partsToDecrypt = new ArrayDeque<Part>(encryptedParts);
|
||||
decryptNextPartOrStartExtractingTextAndAttachments();
|
||||
} else {
|
||||
startExtractingTextAndAttachments(message);
|
||||
}
|
||||
}
|
||||
|
||||
private void decryptNextPartOrStartExtractingTextAndAttachments() {
|
||||
if (partsToDecrypt.isEmpty()) {
|
||||
startExtractingTextAndAttachments(mMessage);
|
||||
return;
|
||||
}
|
||||
|
||||
Part part = partsToDecrypt.peekFirst();
|
||||
if (MessageDecryptor.isPgpMimeEncryptedPart(part)) {
|
||||
startDecryptingPart(part);
|
||||
} else {
|
||||
// Note: We currently only support PGP/MIME multipart/encrypted parts
|
||||
|
||||
partsToDecrypt.removeFirst();
|
||||
decryptNextPartOrStartExtractingTextAndAttachments();
|
||||
}
|
||||
}
|
||||
|
||||
private void startDecryptingPart(Part part) {
|
||||
Multipart multipart = (Multipart) part.getBody();
|
||||
if (multipart == null) {
|
||||
throw new RuntimeException("Downloading missing parts before decryption isn't supported yet");
|
||||
}
|
||||
|
||||
if (!isBoundToCryptoProviderService()) {
|
||||
connectToCryptoProviderService();
|
||||
} else {
|
||||
decryptPart(part);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isBoundToCryptoProviderService() {
|
||||
return openPgpApi != null;
|
||||
}
|
||||
|
||||
private void connectToCryptoProviderService() {
|
||||
String openPgpProvider = mAccount.getOpenPgpProvider();
|
||||
new OpenPgpServiceConnection(getContext(), openPgpProvider,
|
||||
new OnBound() {
|
||||
@Override
|
||||
public void onBound(IOpenPgpService service) {
|
||||
openPgpApi = new OpenPgpApi(getContext(), service);
|
||||
|
||||
decryptNextPartOrStartExtractingTextAndAttachments();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(Exception e) {
|
||||
Log.e(K9.LOG_TAG, "Couldn't connect to OpenPgpService", e);
|
||||
}
|
||||
}).bindToService();
|
||||
}
|
||||
|
||||
private void decryptPart(Part part) {
|
||||
currentlyDecryptingPart = part;
|
||||
decryptVerify(new Intent());
|
||||
}
|
||||
|
||||
private void decryptVerify(Intent intent) {
|
||||
intent.setAction(OpenPgpApi.ACTION_DECRYPT_VERIFY);
|
||||
|
||||
Identity identity = IdentityHelper.getRecipientIdentityFromMessage(mAccount, mMessage);
|
||||
String accountName = OpenPgpApiHelper.buildAccountName(identity);
|
||||
intent.putExtra(OpenPgpApi.EXTRA_ACCOUNT_NAME, accountName);
|
||||
|
||||
try {
|
||||
PipedInputStream pipedInputStream = getPipedInputStreamForEncryptedData();
|
||||
PipedOutputStream decryptedOutputStream = getPipedOutputStreamForDecryptedData();
|
||||
|
||||
openPgpApi.executeApiAsync(intent, pipedInputStream, decryptedOutputStream, new IOpenPgpCallback() {
|
||||
@Override
|
||||
public void onReturn(Intent result) {
|
||||
//TODO: check result code
|
||||
//TODO: signal to AsyncTask in getPipedOutputStreamForDecryptedData() that we have a result code
|
||||
//TODO: handle RESULT_INTENT
|
||||
}
|
||||
});
|
||||
} catch (IOException e) {
|
||||
Log.e(K9.LOG_TAG, "IOException", e);
|
||||
}
|
||||
}
|
||||
|
||||
private PipedInputStream getPipedInputStreamForEncryptedData() throws IOException {
|
||||
PipedInputStream pipedInputStream = new PipedInputStream();
|
||||
|
||||
final PipedOutputStream out = new PipedOutputStream(pipedInputStream);
|
||||
new Thread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
Multipart multipartEncryptedMultipart = (Multipart) currentlyDecryptingPart.getBody();
|
||||
BodyPart encryptionPayloadPart = multipartEncryptedMultipart.getBodyPart(1);
|
||||
Body encryptionPayloadBody = encryptionPayloadPart.getBody();
|
||||
encryptionPayloadBody.writeTo(out);
|
||||
} catch (Exception e) {
|
||||
Log.e(K9.LOG_TAG, "Exception while writing message to crypto provider", e);
|
||||
}
|
||||
}
|
||||
}).start();
|
||||
|
||||
return pipedInputStream;
|
||||
}
|
||||
|
||||
private PipedOutputStream getPipedOutputStreamForDecryptedData() throws IOException {
|
||||
PipedOutputStream decryptedOutputStream = new PipedOutputStream();
|
||||
final PipedInputStream decryptedInputStream = new PipedInputStream(decryptedOutputStream);
|
||||
new AsyncTask<Void, Void, DecryptedBodyPart>() {
|
||||
@Override
|
||||
protected DecryptedBodyPart doInBackground(Void... params) {
|
||||
try {
|
||||
DecryptedBodyPart decryptedPart =
|
||||
DecryptStreamParser.parse(currentlyDecryptingPart, decryptedInputStream);
|
||||
|
||||
//TODO: wait for IOpenPgpCallback.onReturn() to get the result code and only use
|
||||
// decryptedPart when the decryption was successful
|
||||
|
||||
return decryptedPart;
|
||||
} catch (Exception e) {
|
||||
Log.e(K9.LOG_TAG, "Something went wrong while parsing the decrypted MIME part", e);
|
||||
//TODO: pass error to main thread and display error message to user
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(DecryptedBodyPart decryptedPart) {
|
||||
if (decryptedPart == null) {
|
||||
onDecryptionFailed();
|
||||
} else {
|
||||
onDecryptionSuccess(decryptedPart);
|
||||
}
|
||||
}
|
||||
}.execute();
|
||||
return decryptedOutputStream;
|
||||
}
|
||||
|
||||
private void onDecryptionSuccess(DecryptedBodyPart decryptedPart) {
|
||||
addDecryptedPartToMessage(decryptedPart);
|
||||
onDecryptionFinished();
|
||||
}
|
||||
|
||||
private void addDecryptedPartToMessage(DecryptedBodyPart decryptedPart) {
|
||||
Multipart multipart = (Multipart) currentlyDecryptingPart.getBody();
|
||||
multipart.addBodyPart(decryptedPart);
|
||||
}
|
||||
|
||||
private void onDecryptionFailed() {
|
||||
//TODO: display error to user?
|
||||
onDecryptionFinished();
|
||||
}
|
||||
|
||||
private void onDecryptionFinished() {
|
||||
partsToDecrypt.removeFirst();
|
||||
decryptNextPartOrStartExtractingTextAndAttachments();
|
||||
}
|
||||
|
||||
private void onLoadMessageFromDatabaseFailed() {
|
||||
mMessageView.showStatusMessage(mContext.getString(R.string.status_invalid_id_error));
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user