export: support encrypted export, first version
This commit is contained in:
@@ -17,6 +17,20 @@
|
||||
|
||||
package org.sufficientlysecure.keychain.operations;
|
||||
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.PipedInputStream;
|
||||
import java.io.PipedOutputStream;
|
||||
import java.io.PrintStream;
|
||||
import java.security.Security;
|
||||
import java.util.Iterator;
|
||||
|
||||
import android.app.Application;
|
||||
import android.content.ContentResolver;
|
||||
import android.content.ContentValues;
|
||||
import android.net.Uri;
|
||||
|
||||
import org.junit.Assert;
|
||||
import org.junit.Before;
|
||||
import org.junit.BeforeClass;
|
||||
@@ -29,26 +43,39 @@ import org.robolectric.shadows.ShadowLog;
|
||||
import org.spongycastle.bcpg.sig.KeyFlags;
|
||||
import org.spongycastle.jce.provider.BouncyCastleProvider;
|
||||
import org.sufficientlysecure.keychain.WorkaroundBuildConfig;
|
||||
import org.sufficientlysecure.keychain.operations.results.PgpEditKeyResult;
|
||||
import org.sufficientlysecure.keychain.operations.results.DecryptVerifyResult;
|
||||
import org.sufficientlysecure.keychain.operations.results.ExportResult;
|
||||
import org.sufficientlysecure.keychain.operations.results.OperationResult.LogType;
|
||||
import org.sufficientlysecure.keychain.operations.results.OperationResult.OperationLog;
|
||||
import org.sufficientlysecure.keychain.operations.results.PgpEditKeyResult;
|
||||
import org.sufficientlysecure.keychain.pgp.PgpDecryptVerifyInputParcel;
|
||||
import org.sufficientlysecure.keychain.pgp.PgpDecryptVerifyOperation;
|
||||
import org.sufficientlysecure.keychain.pgp.PgpKeyOperation;
|
||||
import org.sufficientlysecure.keychain.pgp.UncachedKeyRing;
|
||||
import org.sufficientlysecure.keychain.pgp.UncachedKeyRing.IteratorWithIOThrow;
|
||||
import org.sufficientlysecure.keychain.pgp.WrappedSignature;
|
||||
import org.sufficientlysecure.keychain.provider.ProviderHelper;
|
||||
import org.sufficientlysecure.keychain.provider.TemporaryStorageProvider;
|
||||
import org.sufficientlysecure.keychain.service.ExportKeyringParcel;
|
||||
import org.sufficientlysecure.keychain.service.SaveKeyringParcel;
|
||||
import org.sufficientlysecure.keychain.service.SaveKeyringParcel.Algorithm;
|
||||
import org.sufficientlysecure.keychain.service.SaveKeyringParcel.ChangeUnlockParcel;
|
||||
import org.sufficientlysecure.keychain.service.input.CryptoInputParcel;
|
||||
import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils;
|
||||
import org.sufficientlysecure.keychain.util.Passphrase;
|
||||
import org.sufficientlysecure.keychain.util.ProgressScaler;
|
||||
import org.sufficientlysecure.keychain.util.TestingUtils;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.PrintStream;
|
||||
import java.security.Security;
|
||||
import java.util.Iterator;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.Matchers.any;
|
||||
import static org.mockito.Matchers.eq;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.spy;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
|
||||
@RunWith(RobolectricGradleTestRunner.class)
|
||||
@Config(constants = WorkaroundBuildConfig.class, sdk = 21, manifest = "src/main/AndroidManifest.xml")
|
||||
@@ -82,7 +109,7 @@ public class ExportTest {
|
||||
parcel.mNewUnlock = new ChangeUnlockParcel(mKeyPhrase1);
|
||||
|
||||
PgpEditKeyResult result = op.createSecretKeyRing(parcel);
|
||||
Assert.assertTrue("initial test key creation must succeed", result.success());
|
||||
assertTrue("initial test key creation must succeed", result.success());
|
||||
Assert.assertNotNull("initial test key creation must succeed", result.getRing());
|
||||
|
||||
mStaticRing1 = result.getRing();
|
||||
@@ -100,7 +127,7 @@ public class ExportTest {
|
||||
parcel.mNewUnlock = new ChangeUnlockParcel(null, new Passphrase("1234"));
|
||||
|
||||
PgpEditKeyResult result = op.createSecretKeyRing(parcel);
|
||||
Assert.assertTrue("initial test key creation must succeed", result.success());
|
||||
assertTrue("initial test key creation must succeed", result.success());
|
||||
Assert.assertNotNull("initial test key creation must succeed", result.getRing());
|
||||
|
||||
mStaticRing2 = result.getRing();
|
||||
@@ -123,17 +150,17 @@ public class ExportTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testExportAll() throws Exception {
|
||||
public void testExportAllLocalStripped() throws Exception {
|
||||
ExportOperation op = new ExportOperation(RuntimeEnvironment.application,
|
||||
new ProviderHelper(RuntimeEnvironment.application), null);
|
||||
|
||||
// make sure there is a local cert (so the later checks that there are none are meaningful)
|
||||
Assert.assertTrue("second keyring has local certification", checkForLocal(mStaticRing2));
|
||||
assertTrue("second keyring has local certification", checkForLocal(mStaticRing2));
|
||||
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||
ExportResult result = op.exportKeysToStream(new OperationLog(), null, false, out);
|
||||
boolean result = op.exportKeysToStream(new OperationLog(), null, false, out);
|
||||
|
||||
Assert.assertTrue("export must be a success", result.success());
|
||||
assertTrue("export must be a success", result);
|
||||
|
||||
long masterKeyId1, masterKeyId2;
|
||||
if (mStaticRing1.getMasterKeyId() < mStaticRing2.getMasterKeyId()) {
|
||||
@@ -148,70 +175,190 @@ public class ExportTest {
|
||||
UncachedKeyRing.fromStream(new ByteArrayInputStream(out.toByteArray()));
|
||||
|
||||
{
|
||||
Assert.assertTrue("export must have two keys (1/2)", unc.hasNext());
|
||||
assertTrue("export must have two keys (1/2)", unc.hasNext());
|
||||
UncachedKeyRing ring = unc.next();
|
||||
Assert.assertEquals("first exported key has correct masterkeyid",
|
||||
masterKeyId1, ring.getMasterKeyId());
|
||||
Assert.assertFalse("first exported key must not be secret", ring.isSecret());
|
||||
Assert.assertFalse("there must be no local signatures in an exported keyring",
|
||||
assertFalse("first exported key must not be secret", ring.isSecret());
|
||||
assertFalse("there must be no local signatures in an exported keyring",
|
||||
checkForLocal(ring));
|
||||
}
|
||||
|
||||
{
|
||||
Assert.assertTrue("export must have two keys (2/2)", unc.hasNext());
|
||||
assertTrue("export must have two keys (2/2)", unc.hasNext());
|
||||
UncachedKeyRing ring = unc.next();
|
||||
Assert.assertEquals("second exported key has correct masterkeyid",
|
||||
masterKeyId2, ring.getMasterKeyId());
|
||||
Assert.assertFalse("second exported key must not be secret", ring.isSecret());
|
||||
Assert.assertFalse("there must be no local signatures in an exported keyring",
|
||||
assertFalse("second exported key must not be secret", ring.isSecret());
|
||||
assertFalse("there must be no local signatures in an exported keyring",
|
||||
checkForLocal(ring));
|
||||
}
|
||||
|
||||
out = new ByteArrayOutputStream();
|
||||
result = op.exportKeysToStream(new OperationLog(), null, true, out);
|
||||
|
||||
Assert.assertTrue("export must be a success", result.success());
|
||||
assertTrue("export must be a success", result);
|
||||
|
||||
unc = UncachedKeyRing.fromStream(new ByteArrayInputStream(out.toByteArray()));
|
||||
|
||||
{
|
||||
Assert.assertTrue("export must have four keys (1/4)", unc.hasNext());
|
||||
assertTrue("export must have four keys (1/4)", unc.hasNext());
|
||||
UncachedKeyRing ring = unc.next();
|
||||
Assert.assertEquals("1/4 exported key has correct masterkeyid",
|
||||
masterKeyId1, ring.getMasterKeyId());
|
||||
Assert.assertFalse("1/4 exported key must not be public", ring.isSecret());
|
||||
Assert.assertFalse("there must be no local signatures in an exported keyring",
|
||||
assertFalse("1/4 exported key must not be public", ring.isSecret());
|
||||
assertFalse("there must be no local signatures in an exported keyring",
|
||||
checkForLocal(ring));
|
||||
|
||||
Assert.assertTrue("export must have four keys (2/4)", unc.hasNext());
|
||||
assertTrue("export must have four keys (2/4)", unc.hasNext());
|
||||
ring = unc.next();
|
||||
Assert.assertEquals("2/4 exported key has correct masterkeyid",
|
||||
masterKeyId1, ring.getMasterKeyId());
|
||||
Assert.assertTrue("2/4 exported key must be public", ring.isSecret());
|
||||
Assert.assertFalse("there must be no local signatures in an exported keyring",
|
||||
assertTrue("2/4 exported key must be public", ring.isSecret());
|
||||
assertFalse("there must be no local signatures in an exported keyring",
|
||||
checkForLocal(ring));
|
||||
}
|
||||
|
||||
{
|
||||
Assert.assertTrue("export must have four keys (3/4)", unc.hasNext());
|
||||
assertTrue("export must have four keys (3/4)", unc.hasNext());
|
||||
UncachedKeyRing ring = unc.next();
|
||||
Assert.assertEquals("3/4 exported key has correct masterkeyid",
|
||||
masterKeyId2, ring.getMasterKeyId());
|
||||
Assert.assertFalse("3/4 exported key must not be public", ring.isSecret());
|
||||
Assert.assertFalse("there must be no local signatures in an exported keyring",
|
||||
assertFalse("3/4 exported key must not be public", ring.isSecret());
|
||||
assertFalse("there must be no local signatures in an exported keyring",
|
||||
checkForLocal(ring));
|
||||
|
||||
Assert.assertTrue("export must have four keys (4/4)", unc.hasNext());
|
||||
assertTrue("export must have four keys (4/4)", unc.hasNext());
|
||||
ring = unc.next();
|
||||
Assert.assertEquals("4/4 exported key has correct masterkeyid",
|
||||
masterKeyId2, ring.getMasterKeyId());
|
||||
Assert.assertTrue("4/4 exported key must be public", ring.isSecret());
|
||||
Assert.assertFalse("there must be no local signatures in an exported keyring",
|
||||
assertTrue("4/4 exported key must be public", ring.isSecret());
|
||||
assertFalse("there must be no local signatures in an exported keyring",
|
||||
checkForLocal(ring));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testExportUnencrypted() throws Exception {
|
||||
|
||||
ContentResolver mockResolver = mock(ContentResolver.class);
|
||||
|
||||
Uri fakeOutputUri = Uri.parse("content://fake/out/1");
|
||||
ByteArrayOutputStream outStream1 = new ByteArrayOutputStream();
|
||||
when(mockResolver.openOutputStream(fakeOutputUri)).thenReturn(outStream1);
|
||||
|
||||
Application spyApplication = spy(RuntimeEnvironment.application);
|
||||
when(spyApplication.getContentResolver()).thenReturn(mockResolver);
|
||||
|
||||
ExportOperation op = new ExportOperation(spyApplication,
|
||||
new ProviderHelper(RuntimeEnvironment.application), null);
|
||||
|
||||
ExportKeyringParcel parcel = new ExportKeyringParcel(null,
|
||||
new long[] { mStaticRing1.getMasterKeyId() }, false, fakeOutputUri);
|
||||
|
||||
ExportResult result = op.execute(parcel, null);
|
||||
|
||||
verify(mockResolver).openOutputStream(fakeOutputUri);
|
||||
|
||||
assertTrue("export must succeed", result.success());
|
||||
|
||||
TestingUtils.assertArrayEqualsPrefix("exported data must start with ascii armor header",
|
||||
"-----BEGIN PGP PUBLIC KEY BLOCK-----\n".getBytes(), outStream1.toByteArray());
|
||||
TestingUtils.assertArrayEqualsSuffix("exported data must end with ascii armor header",
|
||||
"-----END PGP PUBLIC KEY BLOCK-----\n".getBytes(), outStream1.toByteArray());
|
||||
|
||||
{
|
||||
IteratorWithIOThrow<UncachedKeyRing> unc
|
||||
= UncachedKeyRing.fromStream(new ByteArrayInputStream(outStream1.toByteArray()));
|
||||
|
||||
assertTrue("export must have one key", unc.hasNext());
|
||||
UncachedKeyRing ring = unc.next();
|
||||
Assert.assertEquals("exported key has correct masterkeyid",
|
||||
mStaticRing1.getMasterKeyId(), ring.getMasterKeyId());
|
||||
assertFalse("export must have exactly one key", unc.hasNext());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testExportEncrypted() throws Exception {
|
||||
|
||||
|
||||
Application spyApplication;
|
||||
ContentResolver mockResolver = mock(ContentResolver.class);
|
||||
|
||||
Uri fakePipedUri, fakeOutputUri;
|
||||
ByteArrayOutputStream outStream; {
|
||||
|
||||
fakePipedUri = Uri.parse("content://fake/pipe/1");
|
||||
PipedInputStream pipedInStream = new PipedInputStream(8192);
|
||||
PipedOutputStream pipedOutStream = new PipedOutputStream(pipedInStream);
|
||||
when(mockResolver.openOutputStream(fakePipedUri)).thenReturn(pipedOutStream);
|
||||
when(mockResolver.openInputStream(fakePipedUri)).thenReturn(pipedInStream);
|
||||
when(mockResolver.insert(eq(TemporaryStorageProvider.CONTENT_URI), any(ContentValues.class)))
|
||||
.thenReturn(fakePipedUri);
|
||||
|
||||
fakeOutputUri = Uri.parse("content://fake/out/1");
|
||||
outStream = new ByteArrayOutputStream();
|
||||
when(mockResolver.openOutputStream(fakeOutputUri)).thenReturn(outStream);
|
||||
|
||||
spyApplication = spy(RuntimeEnvironment.application);
|
||||
when(spyApplication.getContentResolver()).thenReturn(mockResolver);
|
||||
}
|
||||
|
||||
Passphrase passphrase = new Passphrase("abcde");
|
||||
|
||||
{ // export encrypted
|
||||
ExportOperation op = new ExportOperation(spyApplication,
|
||||
new ProviderHelper(RuntimeEnvironment.application), null);
|
||||
|
||||
ExportKeyringParcel parcel = new ExportKeyringParcel(passphrase,
|
||||
new long[] { mStaticRing1.getMasterKeyId() }, false, fakeOutputUri);
|
||||
|
||||
ExportResult result = op.execute(parcel, null);
|
||||
|
||||
verify(mockResolver).openOutputStream(fakePipedUri);
|
||||
verify(mockResolver).openInputStream(fakePipedUri);
|
||||
verify(mockResolver).openOutputStream(fakeOutputUri);
|
||||
|
||||
assertTrue("export must succeed", result.success());
|
||||
TestingUtils.assertArrayEqualsPrefix("exported data must start with ascii armor header",
|
||||
"-----BEGIN PGP MESSAGE-----\n".getBytes(), outStream.toByteArray());
|
||||
}
|
||||
|
||||
{
|
||||
PgpDecryptVerifyOperation op = new PgpDecryptVerifyOperation(RuntimeEnvironment.application,
|
||||
new ProviderHelper(RuntimeEnvironment.application), null);
|
||||
|
||||
PgpDecryptVerifyInputParcel input = new PgpDecryptVerifyInputParcel(outStream.toByteArray());
|
||||
input.setAllowSymmetricDecryption(true);
|
||||
|
||||
{
|
||||
DecryptVerifyResult result = op.execute(input, new CryptoInputParcel());
|
||||
assertTrue("decryption must return pending without passphrase", result.isPending());
|
||||
Assert.assertTrue("should contain pending passphrase log entry",
|
||||
result.getLog().containsType(LogType.MSG_DC_PENDING_PASSPHRASE));
|
||||
}
|
||||
{
|
||||
DecryptVerifyResult result = op.execute(input, new CryptoInputParcel(new Passphrase("bad")));
|
||||
assertFalse("decryption must fail with bad passphrase", result.success());
|
||||
Assert.assertTrue("should contain bad passphrase log entry",
|
||||
result.getLog().containsType(LogType.MSG_DC_ERROR_SYM_PASSPHRASE));
|
||||
}
|
||||
|
||||
DecryptVerifyResult result = op.execute(input, new CryptoInputParcel(passphrase));
|
||||
assertTrue("decryption must succeed with passphrase", result.success());
|
||||
|
||||
assertEquals("backup filename should be backup_keyid.pub.asc",
|
||||
"backup_" + KeyFormattingUtils.convertKeyIdToHex(mStaticRing1.getMasterKeyId()) + ".pub.asc",
|
||||
result.getDecryptionMetadata().getFilename());
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
/** This function checks whether or not there are any local signatures in a keyring. */
|
||||
private boolean checkForLocal(UncachedKeyRing ring) {
|
||||
Iterator<WrappedSignature> sigs = ring.getPublicKey().getSignatures();
|
||||
|
||||
@@ -19,6 +19,9 @@ package org.sufficientlysecure.keychain.util;
|
||||
|
||||
import java.util.Random;
|
||||
|
||||
import junit.framework.Assert;
|
||||
|
||||
|
||||
public class TestingUtils {
|
||||
public static Passphrase genPassphrase() {
|
||||
return genPassphrase(false);
|
||||
@@ -35,4 +38,25 @@ public class TestingUtils {
|
||||
System.out.println("Generated passphrase: '" + passbuilder.toString() + "'");
|
||||
return new Passphrase(passbuilder.toString());
|
||||
}
|
||||
|
||||
public static void assertArrayEqualsPrefix(String msg, byte[] expected, byte[] actual) {
|
||||
|
||||
Assert.assertTrue("exepected must be shorter or equal to actual array length",
|
||||
expected.length <= actual.length);
|
||||
for (int i = 0; i < expected.length; i++) {
|
||||
Assert.assertEquals(msg, expected[i], actual[i]);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public static void assertArrayEqualsSuffix(String msg, byte[] expected, byte[] actual) {
|
||||
|
||||
Assert.assertTrue("exepected must be shorter or equal to actual array length",
|
||||
expected.length <= actual.length);
|
||||
for (int i = 0; i < expected.length; i++) {
|
||||
Assert.assertEquals(msg, expected[i], actual[actual.length -expected.length +i]);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user