From 46e24058ba02b5302553c031d301484d5cbf2d18 Mon Sep 17 00:00:00 2001 From: Vincent Breitmoser Date: Mon, 28 Sep 2015 00:03:11 +0200 Subject: export: support encrypted export, first version --- .../keychain/operations/ExportOperation.java | 89 +++++++-- .../keychain/operations/results/ExportResult.java | 21 --- .../keychain/service/ExportKeyringParcel.java | 21 +-- .../keychain/ui/BackupCodeEntryFragment.java | 8 +- .../keychain/util/ExportHelper.java | 2 +- .../keychain/operations/ExportTest.java | 209 ++++++++++++++++++--- .../keychain/util/TestingUtils.java | 24 +++ 7 files changed, 295 insertions(+), 79 deletions(-) (limited to 'OpenKeychain') diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/ExportOperation.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/ExportOperation.java index 2eefd869b..3ee5ff771 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/ExportOperation.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/ExportOperation.java @@ -19,16 +19,21 @@ package org.sufficientlysecure.keychain.operations; -import java.io.BufferedOutputStream; import java.io.FileNotFoundException; import java.io.IOException; +import java.io.InputStream; import java.io.OutputStream; +import java.text.SimpleDateFormat; import java.util.Collections; +import java.util.Date; +import java.util.Locale; import java.util.concurrent.atomic.AtomicBoolean; import android.content.Context; import android.database.Cursor; +import android.net.Uri; import android.support.annotation.NonNull; +import android.support.annotation.Nullable; import android.text.TextUtils; import org.spongycastle.bcpg.ArmoredOutputStream; @@ -37,16 +42,21 @@ import org.sufficientlysecure.keychain.R; 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.PgpSignEncryptResult; import org.sufficientlysecure.keychain.pgp.CanonicalizedKeyRing; +import org.sufficientlysecure.keychain.pgp.PgpSignEncryptInputParcel; +import org.sufficientlysecure.keychain.pgp.PgpSignEncryptOperation; import org.sufficientlysecure.keychain.pgp.Progressable; import org.sufficientlysecure.keychain.pgp.UncachedKeyRing; import org.sufficientlysecure.keychain.pgp.exception.PgpGeneralException; import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings; import org.sufficientlysecure.keychain.provider.KeychainDatabase.Tables; import org.sufficientlysecure.keychain.provider.ProviderHelper; +import org.sufficientlysecure.keychain.provider.TemporaryStorageProvider; import org.sufficientlysecure.keychain.service.ExportKeyringParcel; import org.sufficientlysecure.keychain.service.input.CryptoInputParcel; import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils; +import org.sufficientlysecure.keychain.util.InputData; import org.sufficientlysecure.keychain.util.Log; @@ -84,7 +94,7 @@ public class ExportOperation extends BaseOperation { } @NonNull - public ExportResult execute(ExportKeyringParcel exportInput, CryptoInputParcel cryptoInput) { + public ExportResult execute(@NonNull ExportKeyringParcel exportInput, @Nullable CryptoInputParcel cryptoInput) { OperationLog log = new OperationLog(); if (exportInput.mMasterKeyIds != null) { @@ -94,25 +104,83 @@ public class ExportOperation extends BaseOperation { } try { - OutputStream outStream = mProviderHelper.getContentResolver().openOutputStream(exportInput.mOutputUri); - outStream = new BufferedOutputStream(outStream); - return exportKeysToStream(log, exportInput.mMasterKeyIds, exportInput.mExportSecret, outStream); + + boolean nonEncryptedOutput = exportInput.mSymmetricPassphrase == null; + + Uri exportOutputUri = nonEncryptedOutput + ? exportInput.mOutputUri + : TemporaryStorageProvider.createFile(mContext); + + { // export key data, and possibly return if we don't encrypt + + OutputStream outStream = mContext.getContentResolver().openOutputStream(exportOutputUri); + boolean exportSuccess = exportKeysToStream( + log, exportInput.mMasterKeyIds, exportInput.mExportSecret, outStream); + + if (!exportSuccess) { + // if there was an error, it will be in the log so we just have to return + return new ExportResult(ExportResult.RESULT_ERROR, log); + } + + if (nonEncryptedOutput) { + // log.add(LogType.MSG_EXPORT_NO_ENCRYPT, 1); + log.add(LogType.MSG_EXPORT_SUCCESS, 1); + return new ExportResult(ExportResult.RESULT_OK, log); + } + } + + PgpSignEncryptOperation pseOp = new PgpSignEncryptOperation(mContext, mProviderHelper, mProgressable, mCancelled); + + PgpSignEncryptInputParcel inputParcel = new PgpSignEncryptInputParcel(); + inputParcel.setSymmetricPassphrase(exportInput.mSymmetricPassphrase); + inputParcel.setEnableAsciiArmorOutput(true); + + InputStream inStream = mContext.getContentResolver().openInputStream(exportOutputUri); + + String filename; + if (exportInput.mMasterKeyIds.length == 1) { + filename = "backup_" + KeyFormattingUtils.convertKeyIdToHex(exportInput.mMasterKeyIds[0]); + filename += exportInput.mExportSecret ? ".sec.asc" : ".pub.asc"; + } else { + filename = "backup_" + new SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()).format(new Date()); + filename += exportInput.mExportSecret ? ".asc" : ".pub.asc"; + } + + InputData inputData = new InputData(inStream, 0L, filename); + + OutputStream outStream = mContext.getContentResolver().openOutputStream(exportInput.mOutputUri); + + PgpSignEncryptResult encryptResult = pseOp.execute(inputParcel, new CryptoInputParcel(), inputData, outStream); + if (!encryptResult.success()) { + log.addByMerge(encryptResult, 1); + // log.add(LogType.MSG_EXPORT_ERROR_ENCRYPT, 1); + return new ExportResult(ExportResult.RESULT_ERROR, log); + } + + log.add(encryptResult, 1); + log.add(LogType.MSG_EXPORT_SUCCESS, 1); + return new ExportResult(ExportResult.RESULT_OK, log); + } catch (FileNotFoundException e) { log.add(LogType.MSG_EXPORT_ERROR_URI_OPEN, 1); return new ExportResult(ExportResult.RESULT_ERROR, log); + } } - ExportResult exportKeysToStream(OperationLog log, long[] masterKeyIds, boolean exportSecret, OutputStream outStream) { + boolean exportKeysToStream(OperationLog log, long[] masterKeyIds, boolean exportSecret, OutputStream outStream) { - int okSecret = 0, okPublic = 0, progress = 0; + // noinspection unused TODO use these in a log entry + int okSecret = 0, okPublic = 0; + + int progress = 0; Cursor cursor = queryForKeys(masterKeyIds); if (cursor == null || !cursor.moveToFirst()) { log.add(LogType.MSG_EXPORT_ERROR_DB, 1); - return new ExportResult(ExportResult.RESULT_ERROR, log, okPublic, okSecret); + return false; // new ExportResult(ExportResult.RESULT_ERROR, log); } try { @@ -148,7 +216,7 @@ public class ExportOperation extends BaseOperation { } catch (IOException e) { log.add(LogType.MSG_EXPORT_ERROR_IO, 1); - return new ExportResult(ExportResult.RESULT_ERROR, log, okPublic, okSecret); + return false; // new ExportResult(ExportResult.RESULT_ERROR, log); } finally { // Make sure the stream is closed if (outStream != null) try { @@ -159,8 +227,7 @@ public class ExportOperation extends BaseOperation { cursor.close(); } - log.add(LogType.MSG_EXPORT_SUCCESS, 1); - return new ExportResult(ExportResult.RESULT_OK, log, okPublic, okSecret); + return true; } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/results/ExportResult.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/results/ExportResult.java index e21ef949f..135f5af3d 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/results/ExportResult.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/results/ExportResult.java @@ -24,39 +24,18 @@ import org.sufficientlysecure.keychain.service.input.RequiredInputParcel; public class ExportResult extends InputPendingResult { - final int mOkPublic, mOkSecret; - public ExportResult(int result, OperationLog log) { - this(result, log, 0, 0); - } - - public ExportResult(int result, OperationLog log, int okPublic, int okSecret) { super(result, log); - mOkPublic = okPublic; - mOkSecret = okSecret; - } - - - public ExportResult(OperationLog log, RequiredInputParcel requiredInputParcel, - CryptoInputParcel cryptoInputParcel) { - super(log, requiredInputParcel, cryptoInputParcel); - // we won't use these values - mOkPublic = -1; - mOkSecret = -1; } /** Construct from a parcel - trivial because we have no extra data. */ public ExportResult(Parcel source) { super(source); - mOkPublic = source.readInt(); - mOkSecret = source.readInt(); } @Override public void writeToParcel(Parcel dest, int flags) { super.writeToParcel(dest, flags); - dest.writeInt(mOkPublic); - dest.writeInt(mOkSecret); } public static Creator CREATOR = new Creator() { diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/ExportKeyringParcel.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/ExportKeyringParcel.java index 0e990f24c..9b0e5573e 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/ExportKeyringParcel.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/ExportKeyringParcel.java @@ -24,35 +24,31 @@ import android.os.Parcel; import android.os.Parcelable; import org.sufficientlysecure.keychain.pgp.UncachedKeyRing; +import org.sufficientlysecure.keychain.util.Passphrase; + public class ExportKeyringParcel implements Parcelable { - public String mKeyserver; public Uri mCanonicalizedPublicKeyringUri; - public UncachedKeyRing mUncachedKeyRing; + public Passphrase mSymmetricPassphrase; public boolean mExportSecret; public long mMasterKeyIds[]; public Uri mOutputUri; - public ExportKeyringParcel(String keyserver, UncachedKeyRing uncachedKeyRing) { - mKeyserver = keyserver; - mUncachedKeyRing = uncachedKeyRing; - } - - @SuppressWarnings("unused") // TODO: is it used? - public ExportKeyringParcel(long[] masterKeyIds, boolean exportSecret, Uri outputUri) { + public ExportKeyringParcel(Passphrase symmetricPassphrase, + long[] masterKeyIds, boolean exportSecret, Uri outputUri) { + mSymmetricPassphrase = symmetricPassphrase; mMasterKeyIds = masterKeyIds; mExportSecret = exportSecret; mOutputUri = outputUri; } protected ExportKeyringParcel(Parcel in) { - mKeyserver = in.readString(); mCanonicalizedPublicKeyringUri = (Uri) in.readValue(Uri.class.getClassLoader()); - mUncachedKeyRing = (UncachedKeyRing) in.readValue(UncachedKeyRing.class.getClassLoader()); mExportSecret = in.readByte() != 0x00; mOutputUri = (Uri) in.readValue(Uri.class.getClassLoader()); mMasterKeyIds = in.createLongArray(); + mSymmetricPassphrase = in.readParcelable(getClass().getClassLoader()); } @Override @@ -62,12 +58,11 @@ public class ExportKeyringParcel implements Parcelable { @Override public void writeToParcel(Parcel dest, int flags) { - dest.writeString(mKeyserver); dest.writeValue(mCanonicalizedPublicKeyringUri); - dest.writeValue(mUncachedKeyRing); dest.writeByte((byte) (mExportSecret ? 0x01 : 0x00)); dest.writeValue(mOutputUri); dest.writeLongArray(mMasterKeyIds); + dest.writeParcelable(mSymmetricPassphrase, 0); } public static final Parcelable.Creator CREATOR = new Parcelable.Creator() { diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/BackupCodeEntryFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/BackupCodeEntryFragment.java index 83c60e89d..2f32d4912 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/BackupCodeEntryFragment.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/BackupCodeEntryFragment.java @@ -98,6 +98,9 @@ public class BackupCodeEntryFragment extends Fragment implements OnBackStackChan void switchState(BackupCodeState state) { switch (state) { + case STATE_UNINITIALIZED: + throw new AssertionError("can't switch to uninitialized state, this is a bug!"); + case STATE_DISPLAY: mTitleAnimator.setDisplayedChild(0); mStatusAnimator.setDisplayedChild(0); @@ -281,7 +284,8 @@ public class BackupCodeEntryFragment extends Fragment implements OnBackStackChan } - private void animateFlashText(final TextView[] textViews, int color1, int color2, boolean staySecondColor) { + private static void animateFlashText( + final TextView[] textViews, int color1, int color2, boolean staySecondColor) { ValueAnimator anim = ValueAnimator.ofObject(new ArgbEvaluator(), color1, color2); anim.addUpdateListener(new AnimatorUpdateListener() { @@ -299,7 +303,7 @@ public class BackupCodeEntryFragment extends Fragment implements OnBackStackChan } - private void setupEditTextFocusNext(final EditText[] backupCodes) { + private static void setupEditTextFocusNext(final EditText[] backupCodes) { for (int i = 0; i < backupCodes.length -1; i++) { final int next = i+1; diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/ExportHelper.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/ExportHelper.java index a25c75048..2e0b0af36 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/ExportHelper.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/ExportHelper.java @@ -96,7 +96,7 @@ public class ExportHelper @Override public ExportKeyringParcel createOperationInput() { - return new ExportKeyringParcel(mMasterKeyIds, mExportSecret, Uri.fromFile(mExportFile)); + return new ExportKeyringParcel(null, mMasterKeyIds, mExportSecret, Uri.fromFile(mExportFile)); } @Override diff --git a/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/operations/ExportTest.java b/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/operations/ExportTest.java index 305f7a7f8..935ff4b93 100644 --- a/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/operations/ExportTest.java +++ b/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/operations/ExportTest.java @@ -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 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 sigs = ring.getPublicKey().getSignatures(); diff --git a/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/util/TestingUtils.java b/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/util/TestingUtils.java index 1d7952464..0b35aaf22 100644 --- a/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/util/TestingUtils.java +++ b/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/util/TestingUtils.java @@ -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]); + } + + } + } -- cgit v1.2.3