Merge pull request #2645 from k9mail/autocrypt-send-receive

Autocrypt header attach
This commit is contained in:
cketti 2017-09-01 21:21:18 +02:00 committed by GitHub
commit 26a84d549f
12 changed files with 357 additions and 50 deletions

View file

@ -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;

View file

@ -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();

View file

@ -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;
}
}

View file

@ -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;

View file

@ -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;
}
}
}

View file

@ -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);
}
}

View file

@ -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);

View file

@ -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

View file

@ -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);

View file

@ -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);
}
}

View file

@ -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);
}
}

View file

@ -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);
}
}
}