Merge pull request #2645 from k9mail/autocrypt-send-receive
Autocrypt header attach
This commit is contained in:
commit
26a84d549f
12 changed files with 357 additions and 50 deletions
|
@ -22,16 +22,15 @@ public class ComposeCryptoStatus {
|
|||
|
||||
|
||||
private CryptoProviderState cryptoProviderState;
|
||||
private Long signingKeyId;
|
||||
private Long selfEncryptKeyId;
|
||||
private Long openPgpKeyId;
|
||||
private String[] recipientAddresses;
|
||||
private boolean enablePgpInline;
|
||||
private CryptoMode cryptoMode;
|
||||
private RecipientAutocryptStatus recipientAutocryptStatus;
|
||||
|
||||
|
||||
public Long getSigningKeyId() {
|
||||
return signingKeyId;
|
||||
public Long getOpenPgpKeyId() {
|
||||
return openPgpKeyId;
|
||||
}
|
||||
|
||||
CryptoStatusDisplayType getCryptoStatusDisplayType() {
|
||||
|
@ -126,7 +125,8 @@ public class ComposeCryptoStatus {
|
|||
}
|
||||
|
||||
public boolean shouldUsePgpMessageBuilder() {
|
||||
return cryptoProviderState != CryptoProviderState.UNCONFIGURED && (isEncryptionEnabled() || isSignOnly());
|
||||
// CryptoProviderState.ERROR will be handled as an actual error, see SendErrorState
|
||||
return cryptoProviderState != CryptoProviderState.UNCONFIGURED && openPgpKeyId != null;
|
||||
}
|
||||
|
||||
public boolean isEncryptionEnabled() {
|
||||
|
@ -187,8 +187,7 @@ public class ComposeCryptoStatus {
|
|||
|
||||
private CryptoProviderState cryptoProviderState;
|
||||
private CryptoMode cryptoMode;
|
||||
private Long signingKeyId;
|
||||
private Long selfEncryptKeyId;
|
||||
private Long openPgpKeyId;
|
||||
private List<Recipient> recipients;
|
||||
private Boolean enablePgpInline;
|
||||
|
||||
|
@ -202,13 +201,8 @@ public class ComposeCryptoStatus {
|
|||
return this;
|
||||
}
|
||||
|
||||
public ComposeCryptoStatusBuilder setSigningKeyId(Long signingKeyId) {
|
||||
this.signingKeyId = signingKeyId;
|
||||
return this;
|
||||
}
|
||||
|
||||
public ComposeCryptoStatusBuilder setSelfEncryptId(Long selfEncryptKeyId) {
|
||||
this.selfEncryptKeyId = selfEncryptKeyId;
|
||||
public ComposeCryptoStatusBuilder setOpenPgpKeyId(Long openPgpKeyId) {
|
||||
this.openPgpKeyId = openPgpKeyId;
|
||||
return this;
|
||||
}
|
||||
|
||||
|
@ -245,8 +239,7 @@ public class ComposeCryptoStatus {
|
|||
result.cryptoProviderState = cryptoProviderState;
|
||||
result.cryptoMode = cryptoMode;
|
||||
result.recipientAddresses = recipientAddresses.toArray(new String[0]);
|
||||
result.signingKeyId = signingKeyId;
|
||||
result.selfEncryptKeyId = selfEncryptKeyId;
|
||||
result.openPgpKeyId = openPgpKeyId;
|
||||
result.enablePgpInline = enablePgpInline;
|
||||
return result;
|
||||
}
|
||||
|
@ -257,8 +250,7 @@ public class ComposeCryptoStatus {
|
|||
result.cryptoProviderState = cryptoProviderState;
|
||||
result.cryptoMode = cryptoMode;
|
||||
result.recipientAddresses = recipientAddresses;
|
||||
result.signingKeyId = signingKeyId;
|
||||
result.selfEncryptKeyId = selfEncryptKeyId;
|
||||
result.openPgpKeyId = openPgpKeyId;
|
||||
result.enablePgpInline = enablePgpInline;
|
||||
result.recipientAutocryptStatus = recipientAutocryptStatusType;
|
||||
return result;
|
||||
|
|
|
@ -400,8 +400,7 @@ public class RecipientPresenter implements PermissionPingCallback {
|
|||
.setCryptoMode(currentCryptoMode)
|
||||
.setEnablePgpInline(cryptoEnablePgpInline)
|
||||
.setRecipients(getAllRecipients())
|
||||
.setSigningKeyId(accountCryptoKey)
|
||||
.setSelfEncryptId(accountCryptoKey)
|
||||
.setOpenPgpKeyId(accountCryptoKey)
|
||||
.build();
|
||||
|
||||
final String[] recipientAddresses = composeCryptoStatus.getRecipientAddresses();
|
||||
|
|
|
@ -1,13 +1,18 @@
|
|||
package com.fsck.k9.autocrypt;
|
||||
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Map;
|
||||
|
||||
import android.support.annotation.NonNull;
|
||||
|
||||
import okio.ByteString;
|
||||
|
||||
|
||||
class AutocryptHeader {
|
||||
static final String AUTOCRYPT_HEADER = "Autocrypt";
|
||||
|
||||
static final String AUTOCRYPT_PARAM_TO = "addr";
|
||||
static final String AUTOCRYPT_PARAM_ADDR = "addr";
|
||||
static final String AUTOCRYPT_PARAM_KEY_DATA = "keydata";
|
||||
|
||||
static final String AUTOCRYPT_PARAM_TYPE = "type";
|
||||
|
@ -16,16 +21,107 @@ class AutocryptHeader {
|
|||
static final String AUTOCRYPT_PARAM_PREFER_ENCRYPT = "prefer-encrypt";
|
||||
static final String AUTOCRYPT_PREFER_ENCRYPT_MUTUAL = "mutual";
|
||||
|
||||
private static final int HEADER_LINE_LENGTH = 76;
|
||||
|
||||
|
||||
@NonNull
|
||||
final byte[] keyData;
|
||||
@NonNull
|
||||
final String addr;
|
||||
@NonNull
|
||||
final Map<String,String> parameters;
|
||||
final boolean isPreferEncryptMutual;
|
||||
|
||||
AutocryptHeader(Map<String, String> parameters, String addr, byte[] keyData, boolean isPreferEncryptMutual) {
|
||||
AutocryptHeader(@NonNull Map<String, String> parameters, @NonNull String addr,
|
||||
@NonNull byte[] keyData, boolean isPreferEncryptMutual) {
|
||||
this.parameters = parameters;
|
||||
this.addr = addr;
|
||||
this.keyData = keyData;
|
||||
this.isPreferEncryptMutual = isPreferEncryptMutual;
|
||||
}
|
||||
|
||||
String toRawHeaderString() {
|
||||
// TODO we don't properly fold lines here. if we want to support parameters, we need to do that somehow
|
||||
if (!parameters.isEmpty()) {
|
||||
throw new UnsupportedOperationException("arbitrary parameters not supported");
|
||||
}
|
||||
|
||||
StringBuilder builder = new StringBuilder();
|
||||
builder.append(AutocryptHeader.AUTOCRYPT_HEADER).append(": ");
|
||||
builder.append(AutocryptHeader.AUTOCRYPT_PARAM_ADDR).append('=').append(addr).append("; ");
|
||||
if (isPreferEncryptMutual) {
|
||||
builder.append(AutocryptHeader.AUTOCRYPT_PARAM_PREFER_ENCRYPT)
|
||||
.append('=').append(AutocryptHeader.AUTOCRYPT_PREFER_ENCRYPT_MUTUAL).append("; ");
|
||||
}
|
||||
builder.append(AutocryptHeader.AUTOCRYPT_PARAM_KEY_DATA).append("=");
|
||||
|
||||
appendBase64KeyData(builder);
|
||||
|
||||
return builder.toString();
|
||||
}
|
||||
|
||||
private void appendBase64KeyData(StringBuilder builder) {
|
||||
String base64KeyData = ByteString.of(keyData).base64();
|
||||
|
||||
int base64Length = base64KeyData.length();
|
||||
int lineLengthBeforeKeyData = builder.length();
|
||||
int dataLengthInFirstLine = HEADER_LINE_LENGTH -lineLengthBeforeKeyData;
|
||||
|
||||
boolean keyDataFitsInFirstLine = dataLengthInFirstLine > 0 && base64Length < dataLengthInFirstLine;
|
||||
if (keyDataFitsInFirstLine) {
|
||||
builder.append(base64KeyData, 0, base64Length);
|
||||
return;
|
||||
}
|
||||
|
||||
if (dataLengthInFirstLine > 0) {
|
||||
builder.append(base64KeyData, 0, dataLengthInFirstLine).append("\r\n ");
|
||||
} else {
|
||||
builder.append("\r\n ");
|
||||
dataLengthInFirstLine = 0;
|
||||
}
|
||||
|
||||
for (int i = dataLengthInFirstLine; i < base64Length; i += HEADER_LINE_LENGTH) {
|
||||
if (i + HEADER_LINE_LENGTH <= base64Length) {
|
||||
builder.append(base64KeyData, i, i + HEADER_LINE_LENGTH).append("\r\n ");
|
||||
} else {
|
||||
builder.append(base64KeyData, i, base64Length);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) {
|
||||
return true;
|
||||
}
|
||||
if (o == null || getClass() != o.getClass()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
AutocryptHeader that = (AutocryptHeader) o;
|
||||
|
||||
if (isPreferEncryptMutual != that.isPreferEncryptMutual) {
|
||||
return false;
|
||||
}
|
||||
if (!Arrays.equals(keyData, that.keyData)) {
|
||||
return false;
|
||||
}
|
||||
if (!addr.equals(that.addr)) {
|
||||
return false;
|
||||
}
|
||||
if (!parameters.equals(that.parameters)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
int result = Arrays.hashCode(keyData);
|
||||
result = 31 * result + addr.hashCode();
|
||||
result = 31 * result + parameters.hashCode();
|
||||
result = 31 * result + (isPreferEncryptMutual ? 1 : 0);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,6 +6,7 @@ import java.util.Map;
|
|||
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.annotation.VisibleForTesting;
|
||||
|
||||
import com.fsck.k9.mail.Message;
|
||||
import com.fsck.k9.mail.internet.MimeUtility;
|
||||
|
@ -34,7 +35,8 @@ class AutocryptHeaderParser {
|
|||
}
|
||||
|
||||
@Nullable
|
||||
private AutocryptHeader parseAutocryptHeader(String headerValue) {
|
||||
@VisibleForTesting
|
||||
AutocryptHeader parseAutocryptHeader(String headerValue) {
|
||||
Map<String,String> parameters = MimeUtility.getAllHeaderParameters(headerValue);
|
||||
|
||||
String type = parameters.remove(AutocryptHeader.AUTOCRYPT_PARAM_TYPE);
|
||||
|
@ -55,7 +57,7 @@ class AutocryptHeaderParser {
|
|||
return null;
|
||||
}
|
||||
|
||||
String to = parameters.remove(AutocryptHeader.AUTOCRYPT_PARAM_TO);
|
||||
String to = parameters.remove(AutocryptHeader.AUTOCRYPT_PARAM_ADDR);
|
||||
if (to == null) {
|
||||
Timber.e("autocrypt: no to header!");
|
||||
return null;
|
||||
|
|
|
@ -0,0 +1,34 @@
|
|||
package com.fsck.k9.autocrypt;
|
||||
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.InputStream;
|
||||
|
||||
import android.content.Intent;
|
||||
|
||||
import org.openintents.openpgp.util.OpenPgpApi;
|
||||
|
||||
|
||||
public class AutocryptOpenPgpApiInteractor {
|
||||
public static AutocryptOpenPgpApiInteractor getInstance() {
|
||||
return new AutocryptOpenPgpApiInteractor();
|
||||
}
|
||||
|
||||
private AutocryptOpenPgpApiInteractor() { }
|
||||
|
||||
public byte[] getKeyMaterialFromApi(OpenPgpApi openPgpApi, long keyId, String minimizeForUserId) {
|
||||
Intent retreiveKeyIntent = new Intent(OpenPgpApi.ACTION_GET_KEY);
|
||||
retreiveKeyIntent.putExtra(OpenPgpApi.EXTRA_KEY_ID, keyId);
|
||||
retreiveKeyIntent.putExtra(OpenPgpApi.EXTRA_MINIMIZE, true);
|
||||
retreiveKeyIntent.putExtra(OpenPgpApi.EXTRA_MINIMIZE_USER_ID, minimizeForUserId);
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
Intent result = openPgpApi.executeApi(retreiveKeyIntent, (InputStream) null, baos);
|
||||
|
||||
if (result.getIntExtra(OpenPgpApi.RESULT_CODE, OpenPgpApi.RESULT_CODE_ERROR) ==
|
||||
OpenPgpApi.RESULT_CODE_SUCCESS) {
|
||||
return baos.toByteArray();
|
||||
} else{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,6 +1,7 @@
|
|||
package com.fsck.k9.autocrypt;
|
||||
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.Date;
|
||||
|
||||
import android.content.Intent;
|
||||
|
@ -49,4 +50,14 @@ public class AutocryptOperations {
|
|||
public boolean hasAutocryptHeader(Message currentMessage) {
|
||||
return currentMessage.getHeader(AutocryptHeader.AUTOCRYPT_HEADER).length > 0;
|
||||
}
|
||||
|
||||
public void addAutocryptHeaderToMessage(Message message, byte[] keyData,
|
||||
String autocryptAddress, boolean preferEncryptMutual) {
|
||||
AutocryptHeader autocryptHeader = new AutocryptHeader(
|
||||
Collections.<String,String>emptyMap(), autocryptAddress, keyData, preferEncryptMutual);
|
||||
String rawAutocryptHeader = autocryptHeader.toRawHeaderString();
|
||||
|
||||
message.addRawHeader(AutocryptHeader.AUTOCRYPT_HEADER, rawAutocryptHeader);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -14,6 +14,9 @@ import android.support.annotation.VisibleForTesting;
|
|||
|
||||
import com.fsck.k9.Globals;
|
||||
import com.fsck.k9.activity.compose.ComposeCryptoStatus;
|
||||
import com.fsck.k9.autocrypt.AutocryptOpenPgpApiInteractor;
|
||||
import com.fsck.k9.autocrypt.AutocryptOperations;
|
||||
import com.fsck.k9.mail.Address;
|
||||
import com.fsck.k9.mail.Body;
|
||||
import com.fsck.k9.mail.BodyPart;
|
||||
import com.fsck.k9.mail.BoundaryGenerator;
|
||||
|
@ -40,6 +43,11 @@ import timber.log.Timber;
|
|||
public class PgpMessageBuilder extends MessageBuilder {
|
||||
private static final int REQUEST_USER_INTERACTION = 1;
|
||||
|
||||
|
||||
private final AutocryptOperations autocryptOperations;
|
||||
private final AutocryptOpenPgpApiInteractor autocryptOpenPgpApiInteractor;
|
||||
|
||||
|
||||
private OpenPgpApi openPgpApi;
|
||||
|
||||
private MimeMessage currentProcessedMimeMessage;
|
||||
|
@ -50,12 +58,19 @@ public class PgpMessageBuilder extends MessageBuilder {
|
|||
Context context = Globals.getContext();
|
||||
MessageIdGenerator messageIdGenerator = MessageIdGenerator.getInstance();
|
||||
BoundaryGenerator boundaryGenerator = BoundaryGenerator.getInstance();
|
||||
return new PgpMessageBuilder(context, messageIdGenerator, boundaryGenerator);
|
||||
AutocryptOperations autocryptOperations = AutocryptOperations.getInstance();
|
||||
AutocryptOpenPgpApiInteractor autocryptOpenPgpApiInteractor = AutocryptOpenPgpApiInteractor.getInstance();
|
||||
return new PgpMessageBuilder(context, messageIdGenerator, boundaryGenerator, autocryptOperations,
|
||||
autocryptOpenPgpApiInteractor);
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
PgpMessageBuilder(Context context, MessageIdGenerator messageIdGenerator, BoundaryGenerator boundaryGenerator) {
|
||||
PgpMessageBuilder(Context context, MessageIdGenerator messageIdGenerator, BoundaryGenerator boundaryGenerator,
|
||||
AutocryptOperations autocryptOperations, AutocryptOpenPgpApiInteractor autocryptOpenPgpApiInteractor) {
|
||||
super(context, messageIdGenerator, boundaryGenerator);
|
||||
|
||||
this.autocryptOperations = autocryptOperations;
|
||||
this.autocryptOpenPgpApiInteractor = autocryptOpenPgpApiInteractor;
|
||||
}
|
||||
|
||||
|
||||
|
@ -83,6 +98,19 @@ public class PgpMessageBuilder extends MessageBuilder {
|
|||
return;
|
||||
}
|
||||
|
||||
Long openPgpKeyId = cryptoStatus.getOpenPgpKeyId();
|
||||
if (openPgpKeyId == null) {
|
||||
throw new IllegalArgumentException("PgpMessageBuilder requires a configured key id!");
|
||||
}
|
||||
|
||||
Address address = currentProcessedMimeMessage.getFrom()[0];
|
||||
byte[] keyData = autocryptOpenPgpApiInteractor.getKeyMaterialFromApi(
|
||||
openPgpApi, openPgpKeyId, address.getAddress());
|
||||
if (keyData != null) {
|
||||
autocryptOperations.addAutocryptHeaderToMessage(
|
||||
currentProcessedMimeMessage, keyData, address.getAddress(), false);
|
||||
}
|
||||
|
||||
startOrContinueBuildMessage(null);
|
||||
}
|
||||
|
||||
|
@ -135,6 +163,8 @@ public class PgpMessageBuilder extends MessageBuilder {
|
|||
@NonNull
|
||||
private Intent buildOpenPgpApiIntent(boolean shouldSign, boolean shouldEncrypt, boolean isPgpInlineMode) {
|
||||
Intent pgpApiIntent;
|
||||
|
||||
Long openPgpKeyId = cryptoStatus.getOpenPgpKeyId();
|
||||
if (shouldEncrypt) {
|
||||
if (!shouldSign) {
|
||||
throw new IllegalStateException("encrypt-only is not supported at this point and should never happen!");
|
||||
|
@ -142,6 +172,9 @@ public class PgpMessageBuilder extends MessageBuilder {
|
|||
// pgpApiIntent = new Intent(shouldSign ? OpenPgpApi.ACTION_SIGN_AND_ENCRYPT : OpenPgpApi.ACTION_ENCRYPT);
|
||||
pgpApiIntent = new Intent(OpenPgpApi.ACTION_SIGN_AND_ENCRYPT);
|
||||
|
||||
long[] selfEncryptIds = { openPgpKeyId };
|
||||
pgpApiIntent.putExtra(OpenPgpApi.EXTRA_KEY_IDS, selfEncryptIds);
|
||||
|
||||
if(!isDraft()) {
|
||||
pgpApiIntent.putExtra(OpenPgpApi.EXTRA_USER_IDS, cryptoStatus.getRecipientAddresses());
|
||||
// pgpApiIntent.putExtra(OpenPgpApi.EXTRA_ENCRYPT_OPPORTUNISTIC, cryptoStatus.isEncryptionOpportunistic());
|
||||
|
@ -151,7 +184,7 @@ public class PgpMessageBuilder extends MessageBuilder {
|
|||
}
|
||||
|
||||
if (shouldSign) {
|
||||
pgpApiIntent.putExtra(OpenPgpApi.EXTRA_SIGN_KEY_ID, cryptoStatus.getSigningKeyId());
|
||||
pgpApiIntent.putExtra(OpenPgpApi.EXTRA_SIGN_KEY_ID, openPgpKeyId);
|
||||
}
|
||||
|
||||
pgpApiIntent.putExtra(OpenPgpApi.EXTRA_REQUEST_ASCII_ARMOR, true);
|
||||
|
|
|
@ -148,7 +148,7 @@ public class RecipientPresenterTest {
|
|||
|
||||
assertEquals(CryptoStatusDisplayType.NO_CHOICE_EMPTY, status.getCryptoStatusDisplayType());
|
||||
assertTrue(status.isProviderStateOk());
|
||||
assertFalse(status.shouldUsePgpMessageBuilder());
|
||||
assertTrue(status.shouldUsePgpMessageBuilder());
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -161,7 +161,7 @@ public class RecipientPresenterTest {
|
|||
|
||||
assertEquals(CryptoStatusDisplayType.NO_CHOICE_AVAILABLE, status.getCryptoStatusDisplayType());
|
||||
assertTrue(status.isProviderStateOk());
|
||||
assertFalse(status.shouldUsePgpMessageBuilder());
|
||||
assertTrue(status.shouldUsePgpMessageBuilder());
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -174,7 +174,7 @@ public class RecipientPresenterTest {
|
|||
|
||||
assertEquals(CryptoStatusDisplayType.NO_CHOICE_AVAILABLE_TRUSTED, status.getCryptoStatusDisplayType());
|
||||
assertTrue(status.isProviderStateOk());
|
||||
assertFalse(status.shouldUsePgpMessageBuilder());
|
||||
assertTrue(status.shouldUsePgpMessageBuilder());
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -187,7 +187,7 @@ public class RecipientPresenterTest {
|
|||
|
||||
assertEquals(CryptoStatusDisplayType.NO_CHOICE_UNAVAILABLE, status.getCryptoStatusDisplayType());
|
||||
assertTrue(status.isProviderStateOk());
|
||||
assertFalse(status.shouldUsePgpMessageBuilder());
|
||||
assertTrue(status.shouldUsePgpMessageBuilder());
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -217,7 +217,7 @@ public class RecipientPresenterTest {
|
|||
|
||||
assertEquals(CryptoStatusDisplayType.CHOICE_DISABLED_UNTRUSTED, status.getCryptoStatusDisplayType());
|
||||
assertTrue(status.isProviderStateOk());
|
||||
assertFalse(status.shouldUsePgpMessageBuilder());
|
||||
assertTrue(status.shouldUsePgpMessageBuilder());
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
|
@ -105,6 +105,18 @@ public class AutocryptHeaderParserTest {
|
|||
assertEquals("ignore", autocryptHeader.parameters.get("_monkey"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void parseAutocryptHeader_toRawHeaderString() throws Exception {
|
||||
MimeMessage message = parseFromResource("autocrypt/rsa2048-simple.eml");
|
||||
AutocryptHeader autocryptHeader = autocryptHeaderParser.getValidAutocryptHeader(message);
|
||||
|
||||
String headerValue = autocryptHeader.toRawHeaderString();
|
||||
headerValue = headerValue.substring("Autocrypt: ".length());
|
||||
AutocryptHeader parsedAutocryptHeader = autocryptHeaderParser.parseAutocryptHeader(headerValue);
|
||||
|
||||
assertEquals(autocryptHeader, parsedAutocryptHeader);
|
||||
}
|
||||
|
||||
private MimeMessage parseFromResource(String resourceName) throws IOException, MessagingException {
|
||||
InputStream inputStream = readFromResourceFile(resourceName);
|
||||
return MimeMessage.parseMimeMessage(inputStream, false);
|
||||
|
|
|
@ -0,0 +1,56 @@
|
|||
package com.fsck.k9.autocrypt;
|
||||
|
||||
|
||||
import java.util.HashMap;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
|
||||
@SuppressWarnings("WeakerAccess")
|
||||
public class AutocryptHeaderTest {
|
||||
static final HashMap<String, String> PARAMETERS = new HashMap<>();
|
||||
static final String ADDR = "addr";
|
||||
static final String ADDR_LONG = "veryveryverylongaddressthatspansmorethanalinelengthintheheader";
|
||||
static final byte[] KEY_DATA = ("theseare120charactersxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" +
|
||||
"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx").getBytes();
|
||||
static final byte[] KEY_DATA_SHORT = ("theseare15chars").getBytes();
|
||||
static final boolean IS_PREFER_ENCRYPT_MUTUAL = true;
|
||||
|
||||
|
||||
@Test
|
||||
public void toRawHeaderString_returnsExpected() throws Exception {
|
||||
AutocryptHeader autocryptHeader = new AutocryptHeader(PARAMETERS, ADDR, KEY_DATA, IS_PREFER_ENCRYPT_MUTUAL);
|
||||
String autocryptHeaderString = autocryptHeader.toRawHeaderString();
|
||||
|
||||
String expected = "Autocrypt: addr=addr; prefer-encrypt=mutual; keydata=dGhlc2VhcmUxMjBjaGFyYWN\r\n" +
|
||||
" 0ZXJzeHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh\r\n" +
|
||||
" 4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4";
|
||||
assertEquals(expected, autocryptHeaderString);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void toRawHeaderString_withLongAddress_returnsExpected() throws Exception {
|
||||
AutocryptHeader autocryptHeader = new AutocryptHeader(PARAMETERS,
|
||||
ADDR_LONG, KEY_DATA, IS_PREFER_ENCRYPT_MUTUAL);
|
||||
String autocryptHeaderString = autocryptHeader.toRawHeaderString();
|
||||
|
||||
String expected = "Autocrypt: addr=veryveryverylongaddressthatspansmorethanalinelengthintheheader; prefer-encrypt=mutual; keydata=\r\n" +
|
||||
" dGhlc2VhcmUxMjBjaGFyYWN0ZXJzeHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4\r\n" +
|
||||
" eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4\r\n" +
|
||||
" eHh4eHh4";
|
||||
assertEquals(expected, autocryptHeaderString);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void toRawHeaderString_withShortData_returnsExpected() throws Exception {
|
||||
AutocryptHeader autocryptHeader = new AutocryptHeader(PARAMETERS,
|
||||
ADDR, KEY_DATA_SHORT, IS_PREFER_ENCRYPT_MUTUAL);
|
||||
String autocryptHeaderString = autocryptHeader.toRawHeaderString();
|
||||
|
||||
String expected = "Autocrypt: addr=addr; prefer-encrypt=mutual; keydata=dGhlc2VhcmUxNWNoYXJz";
|
||||
assertEquals(expected, autocryptHeaderString);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,23 @@
|
|||
package com.fsck.k9.autocrypt;
|
||||
|
||||
|
||||
import com.fsck.k9.mail.internet.MimeMessage;
|
||||
|
||||
import static junit.framework.Assert.assertEquals;
|
||||
import static junit.framework.Assert.assertNotNull;
|
||||
import static org.junit.Assert.assertArrayEquals;
|
||||
|
||||
|
||||
public class AutocryptOperationsHelper {
|
||||
private static AutocryptHeaderParser INSTANCE = AutocryptHeaderParser.getInstance();
|
||||
|
||||
public static void assertMessageHasAutocryptHeader(
|
||||
MimeMessage message, String addr, boolean isPreferEncryptMutual, byte[] keyData) {
|
||||
AutocryptHeader autocryptHeader = INSTANCE.getValidAutocryptHeader(message);
|
||||
|
||||
assertNotNull(autocryptHeader);
|
||||
assertEquals(addr, autocryptHeader.addr);
|
||||
assertEquals(isPreferEncryptMutual, autocryptHeader.isPreferEncryptMutual);
|
||||
assertArrayEquals(keyData, autocryptHeader.keyData);
|
||||
}
|
||||
}
|
|
@ -23,6 +23,8 @@ import com.fsck.k9.activity.compose.ComposeCryptoStatus.ComposeCryptoStatusBuild
|
|||
import com.fsck.k9.activity.compose.RecipientPresenter.CryptoMode;
|
||||
import com.fsck.k9.activity.compose.RecipientPresenter.CryptoProviderState;
|
||||
import com.fsck.k9.activity.misc.Attachment;
|
||||
import com.fsck.k9.autocrypt.AutocryptOpenPgpApiInteractor;
|
||||
import com.fsck.k9.autocrypt.AutocryptOperations;
|
||||
import com.fsck.k9.mail.Address;
|
||||
import com.fsck.k9.mail.BodyPart;
|
||||
import com.fsck.k9.mail.BoundaryGenerator;
|
||||
|
@ -36,11 +38,11 @@ import com.fsck.k9.mail.internet.TextBody;
|
|||
import com.fsck.k9.message.MessageBuilder.Callback;
|
||||
import com.fsck.k9.message.quote.InsertableHtmlContent;
|
||||
import com.fsck.k9.view.RecipientSelectView.Recipient;
|
||||
|
||||
import org.apache.commons.io.Charsets;
|
||||
import org.apache.commons.io.IOUtils;
|
||||
import org.apache.james.mime4j.util.MimeUtil;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
|
@ -49,6 +51,9 @@ import org.openintents.openpgp.util.OpenPgpApi;
|
|||
import org.openintents.openpgp.util.OpenPgpApi.OpenPgpDataSource;
|
||||
import org.robolectric.RuntimeEnvironment;
|
||||
|
||||
import static com.fsck.k9.autocrypt.AutocryptOperationsHelper.assertMessageHasAutocryptHeader;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.Matchers.any;
|
||||
import static org.mockito.Matchers.anyInt;
|
||||
import static org.mockito.Matchers.eq;
|
||||
|
@ -62,15 +67,22 @@ import static org.mockito.Mockito.when;
|
|||
|
||||
@RunWith(K9RobolectricTestRunner.class)
|
||||
public class PgpMessageBuilderTest {
|
||||
private static final long TEST_SIGN_KEY_ID = 123L;
|
||||
private static final long TEST_SELF_ENCRYPT_KEY_ID = 234L;
|
||||
private static final long TEST_KEY_ID = 123L;
|
||||
private static final String TEST_MESSAGE_TEXT = "message text with a ☭ CCCP symbol";
|
||||
private static final byte[] AUTOCRYPT_KEY_MATERIAL = { 1, 2, 3 };
|
||||
private static final String SENDER_EMAIL = "test@example.org";
|
||||
|
||||
|
||||
private ComposeCryptoStatusBuilder cryptoStatusBuilder = createDefaultComposeCryptoStatusBuilder();
|
||||
private OpenPgpApi openPgpApi = mock(OpenPgpApi.class);
|
||||
private PgpMessageBuilder pgpMessageBuilder = createDefaultPgpMessageBuilder(openPgpApi);
|
||||
private AutocryptOpenPgpApiInteractor autocryptOpenPgpApiInteractor = mock(AutocryptOpenPgpApiInteractor.class);
|
||||
private PgpMessageBuilder pgpMessageBuilder = createDefaultPgpMessageBuilder(openPgpApi, autocryptOpenPgpApiInteractor);
|
||||
|
||||
@Before
|
||||
public void setUp() throws Exception {
|
||||
when(autocryptOpenPgpApiInteractor.getKeyMaterialFromApi(openPgpApi, TEST_KEY_ID, SENDER_EMAIL))
|
||||
.thenReturn(AUTOCRYPT_KEY_MATERIAL);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void build__withCryptoProviderNotOk__shouldThrow() throws MessagingException {
|
||||
|
@ -92,6 +104,32 @@ public class PgpMessageBuilderTest {
|
|||
}
|
||||
}
|
||||
|
||||
@Test(expected = RuntimeException.class)
|
||||
public void buildCleartext__withNoSigningKey__shouldThrow() {
|
||||
cryptoStatusBuilder.setCryptoMode(CryptoMode.NO_CHOICE);
|
||||
cryptoStatusBuilder.setOpenPgpKeyId(null);
|
||||
pgpMessageBuilder.setCryptoStatus(cryptoStatusBuilder.build());
|
||||
|
||||
Callback mockCallback = mock(Callback.class);
|
||||
pgpMessageBuilder.buildAsync(mockCallback);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void buildCleartext__shouldSucceed() {
|
||||
cryptoStatusBuilder.setCryptoMode(CryptoMode.NO_CHOICE);
|
||||
pgpMessageBuilder.setCryptoStatus(cryptoStatusBuilder.build());
|
||||
|
||||
Callback mockCallback = mock(Callback.class);
|
||||
pgpMessageBuilder.buildAsync(mockCallback);
|
||||
|
||||
ArgumentCaptor<MimeMessage> captor = ArgumentCaptor.forClass(MimeMessage.class);
|
||||
verify(mockCallback).onMessageBuildSuccess(captor.capture(), eq(false));
|
||||
verifyNoMoreInteractions(mockCallback);
|
||||
|
||||
MimeMessage message = captor.getValue();
|
||||
assertMessageHasAutocryptHeader(message, SENDER_EMAIL, false, AUTOCRYPT_KEY_MATERIAL);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void buildSign__withNoDetachedSignatureInResult__shouldThrow() throws MessagingException {
|
||||
cryptoStatusBuilder.setCryptoMode(CryptoMode.SIGN_ONLY);
|
||||
|
@ -126,7 +164,7 @@ public class PgpMessageBuilderTest {
|
|||
pgpMessageBuilder.buildAsync(mockCallback);
|
||||
|
||||
Intent expectedIntent = new Intent(OpenPgpApi.ACTION_DETACHED_SIGN);
|
||||
expectedIntent.putExtra(OpenPgpApi.EXTRA_SIGN_KEY_ID, TEST_SIGN_KEY_ID);
|
||||
expectedIntent.putExtra(OpenPgpApi.EXTRA_SIGN_KEY_ID, TEST_KEY_ID);
|
||||
expectedIntent.putExtra(OpenPgpApi.EXTRA_REQUEST_ASCII_ARMOR, true);
|
||||
assertIntentEqualsActionAndExtras(expectedIntent, capturedApiIntent.getValue());
|
||||
|
||||
|
@ -143,7 +181,7 @@ public class PgpMessageBuilderTest {
|
|||
BodyPart contentBodyPart = multipart.getBodyPart(0);
|
||||
Assert.assertEquals("first part must have content type text/plain",
|
||||
"text/plain", MimeUtility.getHeaderParameter(contentBodyPart.getContentType(), null));
|
||||
Assert.assertTrue("signed message body must be TextBody", contentBodyPart.getBody() instanceof TextBody);
|
||||
assertTrue("signed message body must be TextBody", contentBodyPart.getBody() instanceof TextBody);
|
||||
Assert.assertEquals(MimeUtil.ENC_QUOTED_PRINTABLE, ((TextBody) contentBodyPart.getBody()).getEncoding());
|
||||
assertContentOfBodyPartEquals("content must match the message text", contentBodyPart, TEST_MESSAGE_TEXT);
|
||||
|
||||
|
@ -155,6 +193,8 @@ public class PgpMessageBuilderTest {
|
|||
MimeUtility.getHeaderParameter(contentType, "name"));
|
||||
assertContentOfBodyPartEquals("content must match the supplied detached signature",
|
||||
signatureBodyPart, new byte[] { 1, 2, 3 });
|
||||
|
||||
assertMessageHasAutocryptHeader(message, SENDER_EMAIL, false, AUTOCRYPT_KEY_MATERIAL);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -267,7 +307,8 @@ public class PgpMessageBuilderTest {
|
|||
pgpMessageBuilder.buildAsync(mockCallback);
|
||||
|
||||
Intent expectedApiIntent = new Intent(OpenPgpApi.ACTION_SIGN_AND_ENCRYPT);
|
||||
expectedApiIntent.putExtra(OpenPgpApi.EXTRA_SIGN_KEY_ID, TEST_SIGN_KEY_ID);
|
||||
expectedApiIntent.putExtra(OpenPgpApi.EXTRA_SIGN_KEY_ID, TEST_KEY_ID);
|
||||
expectedApiIntent.putExtra(OpenPgpApi.EXTRA_KEY_IDS, new long[] { TEST_KEY_ID });
|
||||
expectedApiIntent.putExtra(OpenPgpApi.EXTRA_REQUEST_ASCII_ARMOR, true);
|
||||
expectedApiIntent.putExtra(OpenPgpApi.EXTRA_USER_IDS, cryptoStatus.getRecipientAddresses());
|
||||
assertIntentEqualsActionAndExtras(expectedApiIntent, capturedApiIntent.getValue());
|
||||
|
@ -292,9 +333,11 @@ public class PgpMessageBuilderTest {
|
|||
BodyPart encryptedBodyPart = multipart.getBodyPart(1);
|
||||
Assert.assertEquals("second part must be octet-stream of encrypted data",
|
||||
"application/octet-stream; name=\"encrypted.asc\"", encryptedBodyPart.getContentType());
|
||||
Assert.assertTrue("message body must be BinaryTempFileBody",
|
||||
assertTrue("message body must be BinaryTempFileBody",
|
||||
encryptedBodyPart.getBody() instanceof BinaryTempFileBody);
|
||||
Assert.assertEquals(MimeUtil.ENC_7BIT, ((BinaryTempFileBody) encryptedBodyPart.getBody()).getEncoding());
|
||||
|
||||
assertMessageHasAutocryptHeader(message, SENDER_EMAIL, false, AUTOCRYPT_KEY_MATERIAL);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -318,7 +361,8 @@ public class PgpMessageBuilderTest {
|
|||
pgpMessageBuilder.buildAsync(mockCallback);
|
||||
|
||||
Intent expectedApiIntent = new Intent(OpenPgpApi.ACTION_SIGN_AND_ENCRYPT);
|
||||
expectedApiIntent.putExtra(OpenPgpApi.EXTRA_SIGN_KEY_ID, TEST_SIGN_KEY_ID);
|
||||
expectedApiIntent.putExtra(OpenPgpApi.EXTRA_SIGN_KEY_ID, TEST_KEY_ID);
|
||||
expectedApiIntent.putExtra(OpenPgpApi.EXTRA_KEY_IDS, new long[] { TEST_KEY_ID });
|
||||
expectedApiIntent.putExtra(OpenPgpApi.EXTRA_REQUEST_ASCII_ARMOR, true);
|
||||
expectedApiIntent.putExtra(OpenPgpApi.EXTRA_USER_IDS, cryptoStatus.getRecipientAddresses());
|
||||
assertIntentEqualsActionAndExtras(expectedApiIntent, capturedApiIntent.getValue());
|
||||
|
@ -329,8 +373,10 @@ public class PgpMessageBuilderTest {
|
|||
|
||||
MimeMessage message = captor.getValue();
|
||||
Assert.assertEquals("text/plain", message.getMimeType());
|
||||
Assert.assertTrue("message body must be BinaryTempFileBody", message.getBody() instanceof BinaryTempFileBody);
|
||||
assertTrue("message body must be BinaryTempFileBody", message.getBody() instanceof BinaryTempFileBody);
|
||||
Assert.assertEquals(MimeUtil.ENC_7BIT, ((BinaryTempFileBody) message.getBody()).getEncoding());
|
||||
|
||||
assertMessageHasAutocryptHeader(message, SENDER_EMAIL, false, AUTOCRYPT_KEY_MATERIAL);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -354,7 +400,7 @@ public class PgpMessageBuilderTest {
|
|||
pgpMessageBuilder.buildAsync(mockCallback);
|
||||
|
||||
Intent expectedApiIntent = new Intent(OpenPgpApi.ACTION_SIGN);
|
||||
expectedApiIntent.putExtra(OpenPgpApi.EXTRA_SIGN_KEY_ID, TEST_SIGN_KEY_ID);
|
||||
expectedApiIntent.putExtra(OpenPgpApi.EXTRA_SIGN_KEY_ID, TEST_KEY_ID);
|
||||
expectedApiIntent.putExtra(OpenPgpApi.EXTRA_REQUEST_ASCII_ARMOR, true);
|
||||
assertIntentEqualsActionAndExtras(expectedApiIntent, capturedApiIntent.getValue());
|
||||
|
||||
|
@ -364,6 +410,8 @@ public class PgpMessageBuilderTest {
|
|||
|
||||
MimeMessage message = captor.getValue();
|
||||
Assert.assertEquals("message must be text/plain", "text/plain", message.getMimeType());
|
||||
|
||||
assertMessageHasAutocryptHeader(message, SENDER_EMAIL, false, AUTOCRYPT_KEY_MATERIAL);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -455,20 +503,21 @@ public class PgpMessageBuilderTest {
|
|||
private ComposeCryptoStatusBuilder createDefaultComposeCryptoStatusBuilder() {
|
||||
return new ComposeCryptoStatusBuilder()
|
||||
.setEnablePgpInline(false)
|
||||
.setSigningKeyId(TEST_SIGN_KEY_ID)
|
||||
.setSelfEncryptId(TEST_SELF_ENCRYPT_KEY_ID)
|
||||
.setOpenPgpKeyId(TEST_KEY_ID)
|
||||
.setRecipients(new ArrayList<Recipient>())
|
||||
.setCryptoProviderState(CryptoProviderState.OK);
|
||||
}
|
||||
|
||||
private static PgpMessageBuilder createDefaultPgpMessageBuilder(OpenPgpApi openPgpApi) {
|
||||
private static PgpMessageBuilder createDefaultPgpMessageBuilder(OpenPgpApi openPgpApi,
|
||||
AutocryptOpenPgpApiInteractor autocryptOpenPgpApiInteractor) {
|
||||
PgpMessageBuilder builder = new PgpMessageBuilder(
|
||||
RuntimeEnvironment.application, MessageIdGenerator.getInstance(), BoundaryGenerator.getInstance());
|
||||
RuntimeEnvironment.application, MessageIdGenerator.getInstance(), BoundaryGenerator.getInstance(),
|
||||
AutocryptOperations.getInstance(), autocryptOpenPgpApiInteractor);
|
||||
builder.setOpenPgpApi(openPgpApi);
|
||||
|
||||
Identity identity = new Identity();
|
||||
identity.setName("tester");
|
||||
identity.setEmail("test@example.org");
|
||||
identity.setEmail(SENDER_EMAIL);
|
||||
identity.setDescription("test identity");
|
||||
identity.setSignatureUse(false);
|
||||
|
||||
|
@ -543,11 +592,11 @@ public class PgpMessageBuilderTest {
|
|||
}
|
||||
if (intentExtra instanceof long[]) {
|
||||
if (!Arrays.equals((long[]) intentExtra, (long[]) expectedExtra)) {
|
||||
Assert.assertArrayEquals((long[]) expectedExtra, (long[]) intentExtra);
|
||||
Assert.assertArrayEquals("error in " + key, (long[]) expectedExtra, (long[]) intentExtra);
|
||||
}
|
||||
} else {
|
||||
if (!intentExtra.equals(expectedExtra)) {
|
||||
Assert.assertEquals(expectedExtra, intentExtra);
|
||||
Assert.assertEquals("error in " + key, expectedExtra, intentExtra);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue