diff --git a/k9mail/src/main/java/com/fsck/k9/autocrypt/AutocryptGossipHeader.java b/k9mail/src/main/java/com/fsck/k9/autocrypt/AutocryptGossipHeader.java index bb0f67766..b63e4259f 100644 --- a/k9mail/src/main/java/com/fsck/k9/autocrypt/AutocryptGossipHeader.java +++ b/k9mail/src/main/java/com/fsck/k9/autocrypt/AutocryptGossipHeader.java @@ -14,9 +14,9 @@ class AutocryptGossipHeader { @NonNull - private final byte[] keyData; + final byte[] keyData; @NonNull - private final String addr; + final String addr; AutocryptGossipHeader(@NonNull String addr, @NonNull byte[] keyData) { this.addr = addr; diff --git a/k9mail/src/main/java/com/fsck/k9/autocrypt/AutocryptGossipHeaderParser.java b/k9mail/src/main/java/com/fsck/k9/autocrypt/AutocryptGossipHeaderParser.java new file mode 100644 index 000000000..b3e9e56eb --- /dev/null +++ b/k9mail/src/main/java/com/fsck/k9/autocrypt/AutocryptGossipHeaderParser.java @@ -0,0 +1,93 @@ +package com.fsck.k9.autocrypt; + + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.annotation.VisibleForTesting; + +import com.fsck.k9.mail.Part; +import com.fsck.k9.mail.internet.MimeUtility; +import okio.ByteString; +import timber.log.Timber; + + +class AutocryptGossipHeaderParser { + private static final AutocryptGossipHeaderParser INSTANCE = new AutocryptGossipHeaderParser(); + + + public static AutocryptGossipHeaderParser getInstance() { + return INSTANCE; + } + + private AutocryptGossipHeaderParser() { } + + + List getAllAutocryptGossipHeaders(Part part) { + String[] headers = part.getHeader(AutocryptGossipHeader.AUTOCRYPT_GOSSIP_HEADER); + List autocryptHeaders = parseAllAutocryptGossipHeaders(headers); + + return Collections.unmodifiableList(autocryptHeaders); + } + + @Nullable + @VisibleForTesting + AutocryptGossipHeader parseAutocryptGossipHeader(String headerValue) { + Map parameters = MimeUtility.getAllHeaderParameters(headerValue); + + String type = parameters.remove(AutocryptHeader.AUTOCRYPT_PARAM_TYPE); + if (type != null && !type.equals(AutocryptHeader.AUTOCRYPT_TYPE_1)) { + Timber.e("autocrypt: unsupported type parameter %s", type); + return null; + } + + String base64KeyData = parameters.remove(AutocryptHeader.AUTOCRYPT_PARAM_KEY_DATA); + if (base64KeyData == null) { + Timber.e("autocrypt: missing key parameter"); + return null; + } + + ByteString byteString = ByteString.decodeBase64(base64KeyData); + if (byteString == null) { + Timber.e("autocrypt: error parsing base64 data"); + return null; + } + + String addr = parameters.remove(AutocryptHeader.AUTOCRYPT_PARAM_ADDR); + if (addr == null) { + Timber.e("autocrypt: no to header!"); + return null; + } + + if (hasCriticalParameters(parameters)) { + return null; + } + + return new AutocryptGossipHeader(addr, byteString.toByteArray()); + } + + private boolean hasCriticalParameters(Map parameters) { + for (String parameterName : parameters.keySet()) { + if (parameterName != null && !parameterName.startsWith("_")) { + return true; + } + } + return false; + } + + @NonNull + private List parseAllAutocryptGossipHeaders(String[] headers) { + ArrayList autocryptHeaders = new ArrayList<>(); + for (String header : headers) { + AutocryptGossipHeader autocryptHeader = parseAutocryptGossipHeader(header); + if (autocryptHeader != null) { + autocryptHeaders.add(autocryptHeader); + } + } + return autocryptHeaders; + } +} diff --git a/k9mail/src/main/java/com/fsck/k9/autocrypt/AutocryptOperations.java b/k9mail/src/main/java/com/fsck/k9/autocrypt/AutocryptOperations.java index ed7990941..ffe4b2466 100644 --- a/k9mail/src/main/java/com/fsck/k9/autocrypt/AutocryptOperations.java +++ b/k9mail/src/main/java/com/fsck/k9/autocrypt/AutocryptOperations.java @@ -1,12 +1,18 @@ package com.fsck.k9.autocrypt; +import java.util.ArrayList; import java.util.Collections; import java.util.Date; +import java.util.List; import android.content.Intent; +import android.os.Bundle; +import android.support.annotation.Nullable; +import com.fsck.k9.mail.Address; import com.fsck.k9.mail.Message; +import com.fsck.k9.mail.Message.RecipientType; import com.fsck.k9.mail.internet.MimeBodyPart; import org.openintents.openpgp.AutocryptPeerUpdate; import org.openintents.openpgp.util.OpenPgpApi; @@ -14,16 +20,20 @@ import org.openintents.openpgp.util.OpenPgpApi; public class AutocryptOperations { private final AutocryptHeaderParser autocryptHeaderParser; + private final AutocryptGossipHeaderParser autocryptGossipHeaderParser; public static AutocryptOperations getInstance() { AutocryptHeaderParser autocryptHeaderParser = AutocryptHeaderParser.getInstance(); - return new AutocryptOperations(autocryptHeaderParser); + AutocryptGossipHeaderParser autocryptGossipHeaderParser = AutocryptGossipHeaderParser.getInstance(); + return new AutocryptOperations(autocryptHeaderParser, autocryptGossipHeaderParser); } - private AutocryptOperations(AutocryptHeaderParser autocryptHeaderParser) { + private AutocryptOperations(AutocryptHeaderParser autocryptHeaderParser, + AutocryptGossipHeaderParser autocryptGossipHeaderParser) { this.autocryptHeaderParser = autocryptHeaderParser; + this.autocryptGossipHeaderParser = autocryptGossipHeaderParser; } public boolean addAutocryptPeerUpdateToIntentIfPresent(Message currentMessage, Intent intent) { @@ -48,10 +58,92 @@ public class AutocryptOperations { return true; } + public boolean addAutocryptGossipUpdateToIntentIfPresent(Message message, MimeBodyPart decryptedPart, Intent intent) { + Bundle updates = createGossipUpdateBundle(message, decryptedPart); + + if (updates == null) { + return false; + } + + intent.putExtra(OpenPgpApi.EXTRA_AUTOCRYPT_PEER_GOSSIP_UPDATES, updates); + return true; + } + + @Nullable + private Bundle createGossipUpdateBundle(Message message, MimeBodyPart decryptedPart) { + List gossipAcceptedAddresses = getGossipAcceptedAddresses(message); + if (gossipAcceptedAddresses.isEmpty()) { + return null; + } + + List autocryptGossipHeaders = + autocryptGossipHeaderParser.getAllAutocryptGossipHeaders(decryptedPart); + if (autocryptGossipHeaders.isEmpty()) { + return null; + } + + Date messageDate = message.getSentDate(); + Date internalDate = message.getInternalDate(); + Date effectiveDate = messageDate.before(internalDate) ? messageDate : internalDate; + + return createGossipUpdateBundle(gossipAcceptedAddresses, autocryptGossipHeaders, effectiveDate); + } + + @Nullable + private Bundle createGossipUpdateBundle(List gossipAcceptedAddresses, + List autocryptGossipHeaders, Date effectiveDate) { + Bundle updates = new Bundle(); + for (AutocryptGossipHeader autocryptGossipHeader : autocryptGossipHeaders) { + boolean isAcceptedAddress = gossipAcceptedAddresses.contains(autocryptGossipHeader.addr.toLowerCase()); + if (!isAcceptedAddress) { + continue; + } + + AutocryptPeerUpdate update = AutocryptPeerUpdate.create(autocryptGossipHeader.keyData, effectiveDate, false); + updates.putParcelable(autocryptGossipHeader.addr, update); + } + if (updates.isEmpty()) { + return null; + } + return updates; + } + + private List getGossipAcceptedAddresses(Message message) { + ArrayList result = new ArrayList<>(); + + addRecipientsToList(result, message, RecipientType.TO); + addRecipientsToList(result, message, RecipientType.CC); + removeRecipientsFromList(result, message, RecipientType.DELIVERED_TO); + + return Collections.unmodifiableList(result); + } + + private void addRecipientsToList(ArrayList result, Message message, RecipientType recipientType) { + for (Address address : message.getRecipients(recipientType)) { + String addr = address.getAddress(); + if (addr != null) { + result.add(addr.toLowerCase()); + } + } + } + + private void removeRecipientsFromList(ArrayList result, Message message, RecipientType recipientType) { + for (Address address : message.getRecipients(recipientType)) { + String addr = address.getAddress(); + if (addr != null) { + result.remove(addr); + } + } + } + public boolean hasAutocryptHeader(Message currentMessage) { return currentMessage.getHeader(AutocryptHeader.AUTOCRYPT_HEADER).length > 0; } + public boolean hasAutocryptGossipHeader(MimeBodyPart part) { + return part.getHeader(AutocryptGossipHeader.AUTOCRYPT_GOSSIP_HEADER).length > 0; + } + public void addAutocryptHeaderToMessage(Message message, byte[] keyData, String autocryptAddress, boolean preferEncryptMutual) { AutocryptHeader autocryptHeader = new AutocryptHeader( diff --git a/k9mail/src/main/java/com/fsck/k9/ui/crypto/MessageCryptoHelper.java b/k9mail/src/main/java/com/fsck/k9/ui/crypto/MessageCryptoHelper.java index bdab6e150..b18ee8141 100644 --- a/k9mail/src/main/java/com/fsck/k9/ui/crypto/MessageCryptoHelper.java +++ b/k9mail/src/main/java/com/fsck/k9/ui/crypto/MessageCryptoHelper.java @@ -555,6 +555,9 @@ public class MessageCryptoHelper { currentCryptoResult.getParcelableExtra(OpenPgpApi.RESULT_DECRYPTION); OpenPgpSignatureResult signatureResult = currentCryptoResult.getParcelableExtra(OpenPgpApi.RESULT_SIGNATURE); + if (decryptionResult.getResult() == OpenPgpDecryptionResult.RESULT_ENCRYPTED) { + parseAutocryptGossipHeadersFromDecryptedPart(outputPart); + } PendingIntent pendingIntent = currentCryptoResult.getParcelableExtra(OpenPgpApi.RESULT_INTENT); PendingIntent insecureWarningPendingIntent = currentCryptoResult.getParcelableExtra(OpenPgpApi.RESULT_INSECURE_DETAIL_INTENT); boolean overrideCryptoWarning = currentCryptoResult.getBooleanExtra( @@ -566,6 +569,26 @@ public class MessageCryptoHelper { onCryptoOperationSuccess(resultAnnotation); } + private void parseAutocryptGossipHeadersFromDecryptedPart(MimeBodyPart outputPart) { + if (!autocryptOperations.hasAutocryptGossipHeader(outputPart)) { + return; + } + + Intent intent = new Intent(OpenPgpApi.ACTION_UPDATE_AUTOCRYPT_PEER); + boolean hasInlineKeyData = autocryptOperations.addAutocryptGossipUpdateToIntentIfPresent( + currentMessage, outputPart, intent); + if (hasInlineKeyData) { + Timber.d("Passing autocrypt data from plain mail to OpenPGP API"); + // We don't care about the result here, so we just call this fire-and-forget wait to minimize delay + openPgpApi.executeApiAsync(intent, null, null, new IOpenPgpCallback() { + @Override + public void onReturn(Intent result) { + Timber.d("Autocrypt update OK!"); + } + }); + } + } + public void onActivityResult(int requestCode, int resultCode, Intent data) { if (isCancelled) { return; diff --git a/plugins/openpgp-api-lib/openpgp-api/src/main/java/org/openintents/openpgp/util/OpenPgpApi.java b/plugins/openpgp-api-lib/openpgp-api/src/main/java/org/openintents/openpgp/util/OpenPgpApi.java index cdf6d4150..560496604 100644 --- a/plugins/openpgp-api-lib/openpgp-api/src/main/java/org/openintents/openpgp/util/OpenPgpApi.java +++ b/plugins/openpgp-api-lib/openpgp-api/src/main/java/org/openintents/openpgp/util/OpenPgpApi.java @@ -308,6 +308,7 @@ public class OpenPgpApi { // UPDATE_AUTOCRYPT_PEER public static final String EXTRA_AUTOCRYPT_PEER_ID = "autocrypt_peer_id"; public static final String EXTRA_AUTOCRYPT_PEER_UPDATE = "autocrypt_peer_update"; + public static final String EXTRA_AUTOCRYPT_PEER_GOSSIP_UPDATES = "autocrypt_peer_gossip_updates"; // INTERNAL, must not be used public static final String EXTRA_CALL_UUID1 = "call_uuid1";