open-keychain/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/operations/BackupOperationTest.java
2018-08-07 00:00:00 +00:00

398 lines
18 KiB
Java

/*
* Copyright (C) 2014 Vincent Breitmoser <v.breitmoser@mugenguild.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
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.Arrays;
import java.util.Iterator;
import android.app.Application;
import android.content.ContentResolver;
import android.content.ContentValues;
import android.net.Uri;
import org.bouncycastle.bcpg.sig.KeyFlags;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.junit.Assert;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.RuntimeEnvironment;
import org.robolectric.shadows.ShadowLog;
import org.sufficientlysecure.keychain.KeychainTestRunner;
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.daos.KeyWritableRepository;
import org.sufficientlysecure.keychain.provider.TemporaryFileProvider;
import org.sufficientlysecure.keychain.service.BackupKeyringParcel;
import org.sufficientlysecure.keychain.service.ChangeUnlockParcel;
import org.sufficientlysecure.keychain.service.SaveKeyringParcel;
import org.sufficientlysecure.keychain.service.SaveKeyringParcel.Algorithm;
import org.sufficientlysecure.keychain.service.SaveKeyringParcel.SubkeyAdd;
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.TestingUtils;
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(KeychainTestRunner.class)
public class BackupOperationTest {
static Passphrase mPassphrase = TestingUtils.testPassphrase0;
static UncachedKeyRing mStaticRing1, mStaticRing2;
static PrintStream oldShadowStream;
@BeforeClass
public static void setUpOnce() throws Exception {
Security.insertProviderAt(new BouncyCastleProvider(), 1);
oldShadowStream = ShadowLog.stream;
// ShadowLog.stream = System.out;
PgpKeyOperation op = new PgpKeyOperation(null);
{
SaveKeyringParcel.Builder builder = SaveKeyringParcel.buildNewKeyringParcel();
builder.addSubkeyAdd(SubkeyAdd.createSubkeyAdd(
Algorithm.ECDSA, 0, SaveKeyringParcel.Curve.NIST_P256, KeyFlags.CERTIFY_OTHER, 0L));
builder.addSubkeyAdd(SubkeyAdd.createSubkeyAdd(
Algorithm.ECDSA, 0, SaveKeyringParcel.Curve.NIST_P256, KeyFlags.SIGN_DATA, 0L));
builder.addSubkeyAdd(SubkeyAdd.createSubkeyAdd(
Algorithm.ECDH, 0, SaveKeyringParcel.Curve.NIST_P256, KeyFlags.ENCRYPT_COMMS, 0L));
builder.addUserId("snips");
builder.setNewUnlock(ChangeUnlockParcel.createUnLockParcelForNewKey(mPassphrase));
PgpEditKeyResult result = op.createSecretKeyRing(builder.build());
assertTrue("initial test key creation must succeed", result.success());
Assert.assertNotNull("initial test key creation must succeed", result.getRing());
mStaticRing1 = result.getRing();
}
{
SaveKeyringParcel.Builder builder = SaveKeyringParcel.buildNewKeyringParcel();
builder.addSubkeyAdd(SubkeyAdd.createSubkeyAdd(
Algorithm.ECDSA, 0, SaveKeyringParcel.Curve.NIST_P256, KeyFlags.CERTIFY_OTHER, 0L));
builder.addSubkeyAdd(SubkeyAdd.createSubkeyAdd(
Algorithm.ECDSA, 0, SaveKeyringParcel.Curve.NIST_P256, KeyFlags.SIGN_DATA, 0L));
builder.addSubkeyAdd(SubkeyAdd.createSubkeyAdd(
Algorithm.ECDH, 0, SaveKeyringParcel.Curve.NIST_P256, KeyFlags.ENCRYPT_COMMS, 0L));
builder.addUserId("snails");
builder.setNewUnlock(ChangeUnlockParcel.createUnLockParcelForNewKey(new Passphrase("1234")));
PgpEditKeyResult result = op.createSecretKeyRing(builder.build());
assertTrue("initial test key creation must succeed", result.success());
Assert.assertNotNull("initial test key creation must succeed", result.getRing());
mStaticRing2 = result.getRing();
mStaticRing2 = UncachedKeyRing.forTestingOnlyAddDummyLocalSignature(mStaticRing2, "1234");
}
}
@Before
public void setUp() {
KeyWritableRepository databaseInteractor =
KeyWritableRepository.create(RuntimeEnvironment.application);
// don't log verbosely here, we're not here to test imports
ShadowLog.stream = oldShadowStream;
databaseInteractor.saveSecretKeyRing(mStaticRing1);
databaseInteractor.saveSecretKeyRing(mStaticRing2);
// ok NOW log verbosely!
ShadowLog.stream = System.out;
}
@Test
public void testExportAllLocalStripped() throws Exception {
BackupOperation op = new BackupOperation(RuntimeEnvironment.application,
KeyWritableRepository.create(RuntimeEnvironment.application), null);
// make sure there is a local cert (so the later checks that there are none are meaningful)
assertTrue("second keyring has local certification", checkForLocal(mStaticRing2));
ByteArrayOutputStream out = new ByteArrayOutputStream();
boolean result = op.exportKeysToStream(new OperationLog(), null, false, true, out, null);
assertTrue("export must be a success", result);
IteratorWithIOThrow<UncachedKeyRing> unc =
UncachedKeyRing.fromStream(new ByteArrayInputStream(out.toByteArray()));
{
assertTrue("export must have two keys (1/2)", unc.hasNext());
UncachedKeyRing ring = unc.next();
Assert.assertEquals("first exported key has correct masterkeyid",
mStaticRing2.getMasterKeyId(), ring.getMasterKeyId());
assertFalse("first exported key must not be secret", ring.isSecret());
assertFalse("there must be no local signatures in an exported keyring",
checkForLocal(ring));
}
{
assertTrue("export must have two keys (2/2)", unc.hasNext());
UncachedKeyRing ring = unc.next();
Assert.assertEquals("second exported key has correct masterkeyid",
mStaticRing1.getMasterKeyId(), ring.getMasterKeyId());
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, true, out, null);
assertTrue("export must be a success", result);
unc = UncachedKeyRing.fromStream(new ByteArrayInputStream(out.toByteArray()));
{
assertTrue("export must have four keys (1/4)", unc.hasNext());
UncachedKeyRing ring = unc.next();
Assert.assertEquals("1/4 exported key has correct masterkeyid",
mStaticRing2.getMasterKeyId(), ring.getMasterKeyId());
assertFalse("1/4 exported key must not be public", ring.isSecret());
assertFalse("there must be no local signatures in an exported keyring",
checkForLocal(ring));
assertTrue("export must have four keys (2/4)", unc.hasNext());
ring = unc.next();
Assert.assertEquals("2/4 exported key has correct masterkeyid",
mStaticRing2.getMasterKeyId(), ring.getMasterKeyId());
assertTrue("2/4 exported key must be public", ring.isSecret());
assertFalse("there must be no local signatures in an exported keyring",
checkForLocal(ring));
}
{
assertTrue("export must have four keys (3/4)", unc.hasNext());
UncachedKeyRing ring = unc.next();
Assert.assertEquals("3/4 exported key has correct masterkeyid",
mStaticRing1.getMasterKeyId(), ring.getMasterKeyId());
assertFalse("3/4 exported key must not be public", ring.isSecret());
assertFalse("there must be no local signatures in an exported keyring",
checkForLocal(ring));
assertTrue("export must have four keys (4/4)", unc.hasNext());
ring = unc.next();
Assert.assertEquals("4/4 exported key has correct masterkeyid",
mStaticRing1.getMasterKeyId(), ring.getMasterKeyId());
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 testExportWithExtraHeaders() throws Exception {
BackupOperation op = new BackupOperation(RuntimeEnvironment.application,
KeyWritableRepository.create(RuntimeEnvironment.application), null);
ByteArrayOutputStream out = new ByteArrayOutputStream();
boolean result = op.exportKeysToStream(
new OperationLog(), new long[] { mStaticRing1.getMasterKeyId() }, true, false,
out, Arrays.asList("header: value"));
assertTrue(result);
String resultData = new String(out.toByteArray());
assertTrue(resultData.startsWith("-----BEGIN PGP PRIVATE KEY BLOCK-----\nheader: value\n\n"));
}
@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);
BackupOperation op = new BackupOperation(spyApplication,
KeyWritableRepository.create(RuntimeEnvironment.application), null);
BackupKeyringParcel parcel = BackupKeyringParcel.create(
new long[] { mStaticRing1.getMasterKeyId() }, false, false, true, 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(TemporaryFileProvider.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
BackupOperation op = new BackupOperation(spyApplication,
KeyWritableRepository.create(RuntimeEnvironment.application), null);
BackupKeyringParcel parcel = BackupKeyringParcel.create(
new long[] { mStaticRing1.getMasterKeyId() }, false, true, true, fakeOutputUri);
CryptoInputParcel inputParcel = CryptoInputParcel.createCryptoInputParcel(passphrase);
ExportResult result = op.execute(parcel, inputParcel);
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,
KeyWritableRepository.create(RuntimeEnvironment.application), null);
PgpDecryptVerifyInputParcel input = PgpDecryptVerifyInputParcel.builder()
.setAllowSymmetricDecryption(true)
.setInputBytes(outStream.toByteArray())
.build();
{
DecryptVerifyResult result = op.execute(input, CryptoInputParcel.createCryptoInputParcel());
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,
CryptoInputParcel.createCryptoInputParcel(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, CryptoInputParcel.createCryptoInputParcel(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());
assertEquals("mime type for pgp keys must be correctly detected",
"application/pgp-keys", result.getDecryptionMetadata().getMimeType());
TestingUtils.assertArrayEqualsPrefix("exported data must start with ascii armor header",
"-----BEGIN PGP PUBLIC KEY BLOCK-----\n".getBytes(), result.getOutputBytes());
TestingUtils.assertArrayEqualsSuffix("exported data must end with ascii armor header",
"-----END PGP PUBLIC KEY BLOCK-----\n".getBytes(), result.getOutputBytes());
{
IteratorWithIOThrow<UncachedKeyRing> unc
= UncachedKeyRing.fromStream(new ByteArrayInputStream(result.getOutputBytes()));
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());
}
}
}
/** 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();
while (sigs.hasNext()) {
if (sigs.next().isLocal()) {
return true;
}
}
return false;
}
}