aboutsummaryrefslogtreecommitdiffstats
path: root/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/pgp/UncachedKeyringMergeTest.java
diff options
context:
space:
mode:
Diffstat (limited to 'OpenKeychain/src/test/java/org/sufficientlysecure/keychain/pgp/UncachedKeyringMergeTest.java')
-rw-r--r--OpenKeychain/src/test/java/org/sufficientlysecure/keychain/pgp/UncachedKeyringMergeTest.java496
1 files changed, 496 insertions, 0 deletions
diff --git a/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/pgp/UncachedKeyringMergeTest.java b/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/pgp/UncachedKeyringMergeTest.java
new file mode 100644
index 000000000..33dae2225
--- /dev/null
+++ b/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/pgp/UncachedKeyringMergeTest.java
@@ -0,0 +1,496 @@
+/*
+ * Copyright (C) 2014 Dominik Schürmann <dominik@dominikschuermann.de>
+ * 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.pgp;
+
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricGradleTestRunner;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.Config;
+import org.robolectric.shadows.ShadowLog;
+import org.spongycastle.bcpg.BCPGInputStream;
+import org.spongycastle.bcpg.PacketTags;
+import org.spongycastle.bcpg.S2K;
+import org.spongycastle.bcpg.SecretKeyPacket;
+import org.spongycastle.bcpg.sig.KeyFlags;
+import org.spongycastle.jce.provider.BouncyCastleProvider;
+import org.spongycastle.util.Strings;
+import org.sufficientlysecure.keychain.BuildConfig;
+import org.sufficientlysecure.keychain.operations.results.OperationResult;
+import org.sufficientlysecure.keychain.operations.results.PgpEditKeyResult;
+import org.sufficientlysecure.keychain.operations.results.OperationResult.OperationLog;
+import org.sufficientlysecure.keychain.pgp.PgpCertifyOperation.PgpCertifyResult;
+import org.sufficientlysecure.keychain.service.CertifyActionsParcel.CertifyAction;
+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.support.KeyringTestingHelper;
+import org.sufficientlysecure.keychain.support.KeyringTestingHelper.RawPacket;
+import org.sufficientlysecure.keychain.util.Passphrase;
+import org.sufficientlysecure.keychain.util.ProgressScaler;
+
+import java.io.ByteArrayInputStream;
+import java.security.Security;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.Iterator;
+import java.util.Random;
+
+/** Tests for the UncachedKeyring.merge method.
+ *
+ * This is another complex, crypto-related method. It merges information from one keyring into
+ * another, keeping information from the base (ie, called object) keyring in case of conflicts.
+ * The types of keys may be Public or Secret and can be mixed, For mixed types the result type
+ * will be the same as the base keyring.
+ *
+ * Test cases:
+ * - Merging keyrings with different masterKeyIds should fail
+ * - Merging a key with itself should be a no-operation
+ * - Merging a key with an extra revocation certificate, it should have that certificate
+ * - Merging a key with an extra user id, it should have that extra user id and its certificates
+ * - Merging a key with an extra user id certificate, it should have that certificate
+ * - Merging a key with an extra subkey, it should have that subkey
+ * - Merging a key with an extra subkey certificate, it should have that certificate
+ * - All of the above operations should work regardless of the key types. This means in particular
+ * that for new subkeys, an equivalent subkey of the proper type must be generated.
+ * - In case of two secret keys with the same id but different S2K, the key of the base keyring
+ * should be preferred (TODO or should it?)
+ *
+ * Note that the merge operation does not care about certificate validity, a bad certificate or
+ * packet will be copied regardless. Filtering out bad packets is done with canonicalization.
+ *
+ */
+@RunWith(RobolectricGradleTestRunner.class)
+@Config(constants = BuildConfig.class, sdk = 21, manifest = "src/main/AndroidManifest.xml")
+public class UncachedKeyringMergeTest {
+
+ static UncachedKeyRing staticRingA, staticRingB;
+ UncachedKeyRing ringA, ringB;
+ ArrayList<RawPacket> onlyA = new ArrayList<RawPacket>();
+ ArrayList<RawPacket> onlyB = new ArrayList<RawPacket>();
+ OperationResult.OperationLog log = new OperationResult.OperationLog();
+ PgpKeyOperation op;
+ SaveKeyringParcel parcel;
+
+ @BeforeClass
+ public static void setUpOnce() throws Exception {
+ Security.insertProviderAt(new BouncyCastleProvider(), 1);
+ ShadowLog.stream = System.out;
+
+ {
+ SaveKeyringParcel parcel = new SaveKeyringParcel();
+ parcel.mAddSubKeys.add(new SaveKeyringParcel.SubkeyAdd(
+ Algorithm.RSA, 1024, null, KeyFlags.CERTIFY_OTHER, 0L));
+ parcel.mAddSubKeys.add(new SaveKeyringParcel.SubkeyAdd(
+ Algorithm.RSA, 1024, null, KeyFlags.SIGN_DATA, 0L));
+
+ parcel.mAddUserIds.add("twi");
+ parcel.mAddUserIds.add("pink");
+ {
+ WrappedUserAttribute uat = WrappedUserAttribute.fromSubpacket(100,
+ "sunshine, sunshine, ladybugs awake~".getBytes());
+ parcel.mAddUserAttribute.add(uat);
+ }
+
+ // passphrase is tested in PgpKeyOperationTest, just use empty here
+ parcel.mNewUnlock = new ChangeUnlockParcel(new Passphrase());
+ PgpKeyOperation op = new PgpKeyOperation(null);
+
+ OperationResult.OperationLog log = new OperationResult.OperationLog();
+
+ PgpEditKeyResult result = op.createSecretKeyRing(parcel);
+ staticRingA = result.getRing();
+ staticRingA = staticRingA.canonicalize(new OperationLog(), 0).getUncachedKeyRing();
+ }
+
+ {
+ SaveKeyringParcel parcel = new SaveKeyringParcel();
+ parcel.mAddSubKeys.add(new SaveKeyringParcel.SubkeyAdd(
+ Algorithm.RSA, 1024, null, KeyFlags.CERTIFY_OTHER, 0L));
+
+ parcel.mAddUserIds.add("shy");
+ // passphrase is tested in PgpKeyOperationTest, just use empty here
+ parcel.mNewUnlock = new ChangeUnlockParcel(new Passphrase());
+ PgpKeyOperation op = new PgpKeyOperation(null);
+
+ OperationResult.OperationLog log = new OperationResult.OperationLog();
+ PgpEditKeyResult result = op.createSecretKeyRing(parcel);
+ staticRingB = result.getRing();
+ staticRingB = staticRingB.canonicalize(new OperationLog(), 0).getUncachedKeyRing();
+ }
+
+ Assert.assertNotNull("initial test key creation must succeed", staticRingA);
+ Assert.assertNotNull("initial test key creation must succeed", staticRingB);
+
+ // we sleep here for a second, to make sure all new certificates have different timestamps
+ Thread.sleep(1000);
+ }
+
+ @Before
+ public void setUp() throws Exception {
+ // show Log.x messages in system.out
+ ShadowLog.stream = System.out;
+ ringA = staticRingA;
+ ringB = staticRingB;
+
+ // setting up some parameters just to reduce code duplication
+ op = new PgpKeyOperation(new ProgressScaler(null, 0, 100, 100));
+
+ // set this up, gonna need it more than once
+ parcel = new SaveKeyringParcel();
+ parcel.mMasterKeyId = ringA.getMasterKeyId();
+ parcel.mFingerprint = ringA.getFingerprint();
+ }
+
+ public void testSelfNoOp() throws Exception {
+
+ UncachedKeyRing merged = mergeWithChecks(ringA, ringA, null);
+ Assert.assertArrayEquals("keyring merged with itself must be identical",
+ ringA.getEncoded(), merged.getEncoded()
+ );
+
+ }
+
+ @Test
+ public void testDifferentMasterKeyIds() throws Exception {
+
+ Assert.assertNotEquals("generated key ids must be different",
+ ringA.getMasterKeyId(), ringB.getMasterKeyId());
+
+ Assert.assertNull("merging keys with differing key ids must fail",
+ ringA.merge(ringB, log, 0));
+ Assert.assertNull("merging keys with differing key ids must fail",
+ ringB.merge(ringA, log, 0));
+
+ }
+
+ @Test
+ public void testAddedUserId() throws Exception {
+
+ UncachedKeyRing modifiedA, modifiedB; {
+ CanonicalizedSecretKeyRing secretRing =
+ new CanonicalizedSecretKeyRing(ringA.getEncoded(), false, 0);
+
+ parcel.reset();
+ parcel.mAddUserIds.add("flim");
+ modifiedA = op.modifySecretKeyRing(secretRing, new CryptoInputParcel(new Passphrase()), parcel).getRing();
+
+ parcel.reset();
+ parcel.mAddUserIds.add("flam");
+ modifiedB = op.modifySecretKeyRing(secretRing, new CryptoInputParcel(new Passphrase()), parcel).getRing();
+ }
+
+ { // merge A into base
+ UncachedKeyRing merged = mergeWithChecks(ringA, modifiedA);
+
+ Assert.assertEquals("merged keyring must have lost no packets", 0, onlyA.size());
+ Assert.assertEquals("merged keyring must have gained two packets", 2, onlyB.size());
+ Assert.assertTrue("merged keyring must contain new user id",
+ merged.getPublicKey().getUnorderedUserIds().contains("flim"));
+ }
+
+ { // merge A into B
+ UncachedKeyRing merged = mergeWithChecks(modifiedA, modifiedB, ringA);
+
+ Assert.assertEquals("merged keyring must have lost no packets", 0, onlyA.size());
+ Assert.assertEquals("merged keyring must have gained four packets", 4, onlyB.size());
+ Assert.assertTrue("merged keyring must contain first new user id",
+ merged.getPublicKey().getUnorderedUserIds().contains("flim"));
+ Assert.assertTrue("merged keyring must contain second new user id",
+ merged.getPublicKey().getUnorderedUserIds().contains("flam"));
+
+ }
+
+ }
+
+ @Test
+ public void testAddedSubkeyId() throws Exception {
+
+ UncachedKeyRing modifiedA, modifiedB;
+ long subKeyIdA, subKeyIdB;
+ {
+ CanonicalizedSecretKeyRing secretRing = new CanonicalizedSecretKeyRing(ringA.getEncoded(), false, 0);
+
+ parcel.reset();
+ parcel.mAddSubKeys.add(new SaveKeyringParcel.SubkeyAdd(
+ Algorithm.RSA, 1024, null, KeyFlags.SIGN_DATA, 0L));
+ modifiedA = op.modifySecretKeyRing(secretRing, new CryptoInputParcel(new Passphrase()), parcel).getRing();
+ modifiedB = op.modifySecretKeyRing(secretRing, new CryptoInputParcel(new Passphrase()), parcel).getRing();
+
+ subKeyIdA = KeyringTestingHelper.getSubkeyId(modifiedA, 2);
+ subKeyIdB = KeyringTestingHelper.getSubkeyId(modifiedB, 2);
+
+ }
+
+ {
+ UncachedKeyRing merged = mergeWithChecks(ringA, modifiedA);
+
+ Assert.assertEquals("merged keyring must have lost no packets", 0, onlyA.size());
+ Assert.assertEquals("merged keyring must have gained two packets", 2, onlyB.size());
+
+ long mergedKeyId = KeyringTestingHelper.getSubkeyId(merged, 2);
+ Assert.assertEquals("merged keyring must contain the new subkey", subKeyIdA, mergedKeyId);
+ }
+
+ {
+ UncachedKeyRing merged = mergeWithChecks(modifiedA, modifiedB, ringA);
+
+ Assert.assertEquals("merged keyring must have lost no packets", 0, onlyA.size());
+ Assert.assertEquals("merged keyring must have gained four packets", 4, onlyB.size());
+
+ Iterator<UncachedPublicKey> it = merged.getPublicKeys();
+ it.next(); it.next();
+ Assert.assertEquals("merged keyring must contain the new subkey",
+ subKeyIdA, it.next().getKeyId());
+ Assert.assertEquals("merged keyring must contain both new subkeys",
+ subKeyIdB, it.next().getKeyId());
+ }
+
+ }
+
+ @Test
+ public void testAddedKeySignature() throws Exception {
+
+ final UncachedKeyRing modified; {
+ parcel.reset();
+ parcel.mRevokeSubKeys.add(KeyringTestingHelper.getSubkeyId(ringA, 1));
+ CanonicalizedSecretKeyRing secretRing = new CanonicalizedSecretKeyRing(
+ ringA.getEncoded(), false, 0);
+ modified = op.modifySecretKeyRing(secretRing, new CryptoInputParcel(new Passphrase()), parcel).getRing();
+ }
+
+ {
+ UncachedKeyRing merged = ringA.merge(modified, log, 0);
+ Assert.assertNotNull("merge must succeed", merged);
+ Assert.assertFalse(
+ "merging keyring with extra signatures into its base should yield that same keyring",
+ KeyringTestingHelper.diffKeyrings(merged.getEncoded(), modified.getEncoded(), onlyA, onlyB)
+ );
+ }
+
+ }
+
+ @Test
+ public void testAddedUserIdSignature() throws Exception {
+
+ final UncachedKeyRing pubRing = ringA.extractPublicKeyRing();
+
+ final UncachedKeyRing modified; {
+ CanonicalizedPublicKeyRing publicRing = new CanonicalizedPublicKeyRing(
+ pubRing.getEncoded(), 0);
+
+ CanonicalizedSecretKey secretKey = new CanonicalizedSecretKeyRing(
+ ringB.getEncoded(), false, 0).getSecretKey();
+ secretKey.unlock(new Passphrase());
+ PgpCertifyOperation op = new PgpCertifyOperation();
+ CertifyAction action = new CertifyAction(pubRing.getMasterKeyId(), publicRing.getPublicKey().getUnorderedUserIds());
+ // sign all user ids
+ PgpCertifyResult result = op.certify(secretKey, publicRing, new OperationLog(), 0, action, null, new Date());
+ Assert.assertTrue("certification must succeed", result.success());
+ Assert.assertNotNull("certification must yield result", result.getCertifiedRing());
+ modified = result.getCertifiedRing();
+ }
+
+ {
+ UncachedKeyRing merged = ringA.merge(modified, log, 0);
+ Assert.assertNotNull("merge must succeed", merged);
+ Assert.assertArrayEquals("foreign signatures should not be merged into secret key",
+ ringA.getEncoded(), merged.getEncoded()
+ );
+ }
+
+ {
+ byte[] sig = KeyringTestingHelper.getNth(
+ modified.getPublicKey().getSignaturesForRawId(Strings.toUTF8ByteArray("twi")), 1).getEncoded();
+
+ // inject the (foreign!) signature into subkey signature position
+ UncachedKeyRing moreModified = KeyringTestingHelper.injectPacket(modified, sig, 1);
+
+ UncachedKeyRing merged = ringA.merge(moreModified, log, 0);
+ Assert.assertNotNull("merge must succeed", merged);
+ Assert.assertArrayEquals("foreign signatures should not be merged into secret key",
+ ringA.getEncoded(), merged.getEncoded()
+ );
+
+ merged = pubRing.merge(moreModified, log, 0);
+ Assert.assertNotNull("merge must succeed", merged);
+ Assert.assertTrue(
+ "merged keyring should contain new signature",
+ KeyringTestingHelper.diffKeyrings(pubRing.getEncoded(), merged.getEncoded(), onlyA, onlyB)
+ );
+ Assert.assertEquals("merged keyring should be missing no packets", 0, onlyA.size());
+ Assert.assertEquals("merged keyring should contain exactly two more packets", 2, onlyB.size());
+ Assert.assertEquals("first added packet should be a signature",
+ PacketTags.SIGNATURE, onlyB.get(0).tag);
+ Assert.assertEquals("first added packet should be in the position we injected it at",
+ 1, onlyB.get(0).position);
+ Assert.assertEquals("second added packet should be a signature",
+ PacketTags.SIGNATURE, onlyB.get(1).tag);
+
+ }
+
+ {
+ UncachedKeyRing merged = pubRing.merge(modified, log, 0);
+ Assert.assertNotNull("merge must succeed", merged);
+ Assert.assertFalse(
+ "merging keyring with extra signatures into its base should yield that same keyring",
+ KeyringTestingHelper.diffKeyrings(merged.getEncoded(), modified.getEncoded(), onlyA, onlyB)
+ );
+ }
+ }
+
+ @Test
+ public void testAddedUserAttributeSignature() throws Exception {
+
+ final UncachedKeyRing modified; {
+ parcel.reset();
+
+ Random r = new Random();
+ int type = r.nextInt(110)+1;
+ byte[] data = new byte[r.nextInt(2000)];
+ new Random().nextBytes(data);
+
+ WrappedUserAttribute uat = WrappedUserAttribute.fromSubpacket(type, data);
+ parcel.mAddUserAttribute.add(uat);
+
+ CanonicalizedSecretKeyRing secretRing = new CanonicalizedSecretKeyRing(
+ ringA.getEncoded(), false, 0);
+ modified = op.modifySecretKeyRing(secretRing, new CryptoInputParcel(new Passphrase()), parcel).getRing();
+ }
+
+ {
+ UncachedKeyRing merged = ringA.merge(modified, log, 0);
+ Assert.assertNotNull("merge must succeed", merged);
+ Assert.assertFalse(
+ "merging keyring with extra user attribute into its base should yield that same keyring",
+ KeyringTestingHelper.diffKeyrings(merged.getEncoded(), modified.getEncoded(), onlyA, onlyB)
+ );
+ }
+
+ }
+
+ private UncachedKeyRing mergeWithChecks(UncachedKeyRing a, UncachedKeyRing b)
+ throws Exception {
+ return mergeWithChecks(a, b, a);
+ }
+
+ private UncachedKeyRing mergeWithChecks(UncachedKeyRing a, UncachedKeyRing b,
+ UncachedKeyRing base)
+ throws Exception {
+
+ Assert.assertTrue("merging keyring must be secret type", a.isSecret());
+ Assert.assertTrue("merged keyring must be secret type", b.isSecret());
+
+ final UncachedKeyRing resultA;
+ UncachedKeyRing resultB;
+
+ { // sec + sec
+ resultA = a.merge(b, log, 0);
+ Assert.assertNotNull("merge must succeed as sec(a)+sec(b)", resultA);
+
+ resultB = b.merge(a, log, 0);
+ Assert.assertNotNull("merge must succeed as sec(b)+sec(a)", resultB);
+
+ // check commutativity, if requested
+ Assert.assertFalse("result of merge must be commutative",
+ KeyringTestingHelper.diffKeyrings(
+ resultA.getEncoded(), resultB.getEncoded(), onlyA, onlyB)
+ );
+ }
+
+ final UncachedKeyRing pubA = a.extractPublicKeyRing();
+ final UncachedKeyRing pubB = b.extractPublicKeyRing();
+
+ { // sec + pub
+
+ // this one is special, because GNU_DUMMY keys might be generated!
+
+ resultB = a.merge(pubB, log, 0);
+ Assert.assertNotNull("merge must succeed as sec(a)+pub(b)", resultA);
+
+ // these MAY diff
+ KeyringTestingHelper.diffKeyrings(resultA.getEncoded(), resultB.getEncoded(),
+ onlyA, onlyB);
+
+ Assert.assertEquals("sec(a)+pub(b): results must have equal number of packets",
+ onlyA.size(), onlyB.size());
+
+ for (int i = 0; i < onlyA.size(); i++) {
+ Assert.assertEquals("sec(a)+pub(c): old packet must be secret subkey",
+ PacketTags.SECRET_SUBKEY, onlyA.get(i).tag);
+ Assert.assertEquals("sec(a)+pub(c): new packet must be dummy secret subkey",
+ PacketTags.SECRET_SUBKEY, onlyB.get(i).tag);
+
+ SecretKeyPacket pA = (SecretKeyPacket) new BCPGInputStream(new ByteArrayInputStream(onlyA.get(i).buf)).readPacket();
+ SecretKeyPacket pB = (SecretKeyPacket) new BCPGInputStream(new ByteArrayInputStream(onlyB.get(i).buf)).readPacket();
+
+ Assert.assertArrayEquals("sec(a)+pub(c): both packets must have equal pubkey parts",
+ pA.getPublicKeyPacket().getEncoded(), pB.getPublicKeyPacket().getEncoded()
+ );
+
+ Assert.assertEquals("sec(a)+pub(c): new packet should have GNU_DUMMY S2K type",
+ S2K.GNU_DUMMY_S2K, pB.getS2K().getType());
+ Assert.assertEquals("sec(a)+pub(c): new packet should have GNU_DUMMY protection mode 0x1",
+ 0x1, pB.getS2K().getProtectionMode());
+ Assert.assertEquals("sec(a)+pub(c): new packet secret key data should have length zero",
+ 0, pB.getSecretKeyData().length);
+ Assert.assertNull("sec(a)+pub(c): new packet should have no iv data", pB.getIV());
+
+ }
+
+ }
+
+ { // pub + sec, and pub + pub
+ final UncachedKeyRing pubResult = resultA.extractPublicKeyRing();
+
+ resultB = pubA.merge(b, log, 0);
+ Assert.assertNotNull("merge must succeed as pub(a)+sec(b)", resultA);
+
+ Assert.assertFalse("result of pub(a)+sec(b) must be same as pub(sec(a)+sec(b))",
+ KeyringTestingHelper.diffKeyrings(
+ pubResult.getEncoded(), resultB.getEncoded(), onlyA, onlyB)
+ );
+
+ resultB = pubA.merge(pubB, log, 0);
+ Assert.assertNotNull("merge must succeed as pub(a)+pub(b)", resultA);
+
+ Assert.assertFalse("result of pub(a)+pub(b) must be same as pub(sec(a)+sec(b))",
+ KeyringTestingHelper.diffKeyrings(
+ pubResult.getEncoded(), resultB.getEncoded(), onlyA, onlyB)
+ );
+
+ }
+
+ if (base != null) {
+ // set up onlyA and onlyB to be a diff to the base
+ Assert.assertTrue("merged keyring must differ from base",
+ KeyringTestingHelper.diffKeyrings(
+ base.getEncoded(), resultA.getEncoded(), onlyA, onlyB)
+ );
+ }
+
+ return resultA;
+
+ }
+
+}